diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 00000000..9d49aa41
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,13 @@
+- With issues:
+ - Use the search tool before opening a new issue.
+ - Please provide source code and commit sha if you found a bug.
+ - Review existing issues and provide feedback or react to them.
+
+- go version:
+- gin version (or commit ref):
+- operating system:
+
+## Description
+
+## Screenshots
+
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..8630bc35
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,7 @@
+- With pull requests:
+ - Open your pull request against `master`
+ - Your pull request should have no more than two commits, if not you should squash them.
+ - It should pass all tests in the available continuous integrations systems such as TravisCI.
+ - You should add/modify tests to cover your proposed code changes.
+ - If your pull request contains a new feature, please document it on the README.
+
diff --git a/.gitignore b/.gitignore
index 14dc8f20..bdd50c95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@ vendor/*
coverage.out
count.out
test
+profile.out
+tmp.out
diff --git a/.travis.yml b/.travis.yml
index ec12cad6..be196feb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,13 +5,29 @@ go:
- 1.7.x
- 1.8.x
- 1.9.x
+ - 1.10.x
+ - 1.11.x
+ - 1.12.x
- master
+matrix:
+ fast_finish: true
+ include:
+ - go: 1.11.x
+ env: GO111MODULE=on
+ - go: 1.12.x
+ env: GO111MODULE=on
+
git:
- depth: 3
+ depth: 10
+
+before_install:
+ - if [[ "${GO111MODULE}" = "on" ]]; then mkdir "${HOME}/go"; export GOPATH="${HOME}/go"; fi
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
diff --git a/AUTHORS.md b/AUTHORS.md
index 7ab7213d..dda19bcf 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,8 +1,12 @@
List of all the awesome people working to make Gin the best Web Framework in Go.
+## gin 1.x series authors
+
+**Gin Core Team:** Bo-Yi Wu (@appleboy), 田欧 (@thinkerou), Javier Provecho (@javierprovecho)
+
## gin 0.x series authors
-**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
+**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
People and companies, who have contributed, in alphabetical order.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ee485ec3..e6a108ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,28 @@
# CHANGELOG
-### Gin 1.2
+### Gin 1.3.0
+
+- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
+- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
+- [NEW] Add `Pusher()` in [`type ResponseWriter`](https://godoc.org/github.com/gin-gonic/gin#ResponseWriter) for supporting http2 push, see [#1273](https://github.com/gin-gonic/gin/pull/1273)
+- [NEW] Add [`func (*Context) DataFromReader`](https://godoc.org/github.com/gin-gonic/gin#Context.DataFromReader) for serving dynamic data, see [#1304](https://github.com/gin-gonic/gin/pull/1304)
+- [NEW] Add [`func (*Context) ShouldBindBodyWith`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindBodyWith) allowing to call binding multiple times, see [#1341](https://github.com/gin-gonic/gin/pull/1341)
+- [NEW] Support pointers in form binding, see [#1336](https://github.com/gin-gonic/gin/pull/1336)
+- [NEW] Add [`func (*Context) JSONP`](https://godoc.org/github.com/gin-gonic/gin#Context.JSONP), see [#1333](https://github.com/gin-gonic/gin/pull/1333)
+- [NEW] Support default value in form binding, see [#1138](https://github.com/gin-gonic/gin/pull/1138)
+- [NEW] Expose validator engine in [`type StructValidator`](https://godoc.org/github.com/gin-gonic/gin/binding#StructValidator), see [#1277](https://github.com/gin-gonic/gin/pull/1277)
+- [NEW] Add [`func (*Context) ShouldBind`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBind), [`func (*Context) ShouldBindQuery`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindQuery) and [`func (*Context) ShouldBindJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindJSON), see [#1047](https://github.com/gin-gonic/gin/pull/1047)
+- [NEW] Add support for `time.Time` location in form binding, see [#1117](https://github.com/gin-gonic/gin/pull/1117)
+- [NEW] Add [`func (*Context) BindQuery`](https://godoc.org/github.com/gin-gonic/gin#Context.BindQuery), see [#1029](https://github.com/gin-gonic/gin/pull/1029)
+- [NEW] Make [jsonite](https://github.com/json-iterator/go) optional with build tags, see [#1026](https://github.com/gin-gonic/gin/pull/1026)
+- [NEW] Show query string in logger, see [#999](https://github.com/gin-gonic/gin/pull/999)
+- [NEW] Add [`func (*Context) SecureJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.SecureJSON), see [#987](https://github.com/gin-gonic/gin/pull/987) and [#993](https://github.com/gin-gonic/gin/pull/993)
+- [DEPRECATE] `func (*Context) GetCookie` for [`func (*Context) Cookie`](https://godoc.org/github.com/gin-gonic/gin#Context.Cookie)
+- [FIX] Don't display color tags if [`func DisableConsoleColor`](https://godoc.org/github.com/gin-gonic/gin#DisableConsoleColor) called, see [#1072](https://github.com/gin-gonic/gin/pull/1072)
+- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250)
+- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460)
+
+### Gin 1.2.0
- [NEW] Switch from godeps to govendor
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls
diff --git a/Makefile b/Makefile
index 5468563a..7ab57e27 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,9 @@
+GO ?= go
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/)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
+TESTFOLDER := $(shell $(GO) list ./... | grep -E 'gin$$|binding$$|render$$' | grep -v examples)
all: install
@@ -9,7 +12,25 @@ install: deps
.PHONY: 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 1; \
+ elif grep -q "setup failed" tmp.out; then \
+ rm tmp.out; \
+ exit 1; \
+ fi; \
+ if [ -f profile.out ]; then \
+ cat profile.out | grep -v "mode:" >> coverage.out; \
+ rm profile.out; \
+ fi; \
+ done
.PHONY: fmt
fmt:
@@ -17,7 +38,6 @@ fmt:
.PHONY: fmt-check
fmt-check:
- # get all go files and run go fmt on them
@diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
@@ -26,14 +46,14 @@ fmt-check:
fi;
vet:
- go vet $(PACKAGES)
+ $(GO) vet $(VETPACKAGES)
deps:
@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
@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
embedmd:
@@ -42,20 +62,26 @@ embedmd:
.PHONY: lint
lint:
@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
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: misspell-check
misspell-check:
@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
misspell -error $(GOFILES)
.PHONY: misspell
misspell:
@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
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;
diff --git a/README.md b/README.md
index 6e421f02..058eee2a 100644
--- a/README.md
+++ b/README.md
@@ -3,15 +3,129 @@
[](https://travis-ci.org/gin-gonic/gin)
- [](https://codecov.io/gh/gin-gonic/gin)
- [](https://goreportcard.com/report/github.com/gin-gonic/gin)
- [](https://godoc.org/github.com/gin-gonic/gin)
- [](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://codecov.io/gh/gin-gonic/gin)
+[](https://goreportcard.com/report/github.com/gin-gonic/gin)
+[](https://godoc.org/github.com/gin-gonic/gin)
+[](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
+[](https://www.codetriage.com/gin-gonic/gin)
+[](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.

+## Contents
+
+- [Installation](#installation)
+- [Prerequisite](#prerequisite)
+- [Quick start](#quick-start)
+- [Benchmarks](#benchmarks)
+- [Gin v1.stable](#gin-v1-stable)
+- [Build with jsoniter](#build-with-jsoniter)
+- [API Examples](#api-examples)
+ - [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
+ - [Parameters in path](#parameters-in-path)
+ - [Querystring parameters](#querystring-parameters)
+ - [Multipart/Urlencoded Form](#multiparturlencoded-form)
+ - [Another example: query + post form](#another-example-query--post-form)
+ - [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
+ - [Upload files](#upload-files)
+ - [Grouping routes](#grouping-routes)
+ - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
+ - [Using middleware](#using-middleware)
+ - [How to write log file](#how-to-write-log-file)
+ - [Custom Log Format](#custom-log-format)
+ - [Model binding and validation](#model-binding-and-validation)
+ - [Custom Validators](#custom-validators)
+ - [Only Bind Query String](#only-bind-query-string)
+ - [Bind Query String or Post Data](#bind-query-string-or-post-data)
+ - [Bind Uri](#bind-uri)
+ - [Bind HTML checkboxes](#bind-html-checkboxes)
+ - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
+ - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
+ - [JSONP rendering](#jsonp)
+ - [Serving static files](#serving-static-files)
+ - [Serving data from reader](#serving-data-from-reader)
+ - [HTML rendering](#html-rendering)
+ - [Multitemplate](#multitemplate)
+ - [Redirects](#redirects)
+ - [Custom Middleware](#custom-middleware)
+ - [Using BasicAuth() middleware](#using-basicauth-middleware)
+ - [Goroutines inside a middleware](#goroutines-inside-a-middleware)
+ - [Custom HTTP configuration](#custom-http-configuration)
+ - [Support Let's Encrypt](#support-lets-encrypt)
+ - [Run multiple service using Gin](#run-multiple-service-using-gin)
+ - [Graceful restart or stop](#graceful-restart-or-stop)
+ - [Build a single binary with templates](#build-a-single-binary-with-templates)
+ - [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)
+ - [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)
+- [Users](#users)
+
+## Installation
+
+To install Gin package, you need to install Go and set your Go workspace first.
+
+1. Download and install it:
+
+```sh
+$ go get -u github.com/gin-gonic/gin
+```
+
+2. Import it in your code:
+
+```go
+import "github.com/gin-gonic/gin"
+```
+
+3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
+
+```go
+import "net/http"
+```
+
+### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor)
+
+1. `go get` govendor
+
+```sh
+$ go get github.com/kardianos/govendor
+```
+2. Create your project folder and `cd` inside
+
+```sh
+$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
+```
+
+3. Vendor init your project and add gin
+
+```sh
+$ govendor init
+$ govendor fetch github.com/gin-gonic/gin@v1.3
+```
+
+4. Copy a starting template inside your project
+
+```sh
+$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go
+```
+
+5. Run your project
+
+```sh
+$ go run main.go
+```
+
+## Prerequisite
+
+Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
+
+## Quick start
+
```sh
# assume the following codes in example.go file
$ cat example.go
@@ -87,61 +201,9 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894
- [x] Battle tested
- [x] API frozen, new releases will not break your code.
-## Start using it
-
-1. Download and install it:
-
-```sh
-$ go get github.com/gin-gonic/gin
-```
-
-2. Import it in your code:
-
-```go
-import "github.com/gin-gonic/gin"
-```
-
-3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
-
-```go
-import "net/http"
-```
-
-### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor)
-
-1. `go get` govendor
-
-```sh
-$ go get github.com/kardianos/govendor
-```
-2. Create your project folder and `cd` inside
-
-```sh
-$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
-```
-
-3. Vendor init your project and add gin
-
-```sh
-$ govendor init
-$ govendor fetch github.com/gin-gonic/gin@v1.2
-```
-
-4. Copy a starting template inside your project
-
-```sh
-$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go
-```
-
-5. Run your project
-
-```sh
-$ go run main.go
-```
-
## Build with [jsoniter](https://github.com/json-iterator/go)
-Gin use `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags.
+Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags.
```sh
$ go build -tags=jsoniter .
@@ -153,9 +215,6 @@ $ go build -tags=jsoniter .
```go
func main() {
- // Disable Console Color
- // gin.DisableConsoleColor()
-
// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()
@@ -181,7 +240,7 @@ func main() {
func main() {
router := gin.Default()
- // This handler will match /user/john but will not match neither /user/ or /user
+ // This handler will match /user/john but will not match /user/ or /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
@@ -268,12 +327,44 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great
```
+### Map as querystring or postform parameters
+
+```
+POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
+Content-Type: application/x-www-form-urlencoded
+
+names[first]=thinkerou&names[second]=tianou
+```
+
+```go
+func main() {
+ router := gin.Default()
+
+ router.POST("/post", func(c *gin.Context) {
+
+ ids := c.QueryMap("ids")
+ names := c.PostFormMap("names")
+
+ fmt.Printf("ids: %v; names: %v", ids, names)
+ })
+ router.Run(":8080")
+}
+```
+
+```
+ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
+```
+
### Upload files
#### Single file
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
func main() {
router := gin.Default()
@@ -439,9 +530,88 @@ 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" "
+```
+
+### Controlling Log output coloring
+
+By default, logs output on console should be colorized depending on the detected TTY.
+
+Never colorize logs:
+
+```go
+func main() {
+ // Disable log's color
+ gin.DisableConsoleColor()
+
+ // Creates a gin router with default middleware:
+ // logger and recovery (crash-free) middleware
+ router := gin.Default()
+
+ router.GET("/ping", func(c *gin.Context) {
+ c.String(200, "pong")
+ })
+
+ router.Run(":8080")
+}
+```
+
+Always colorize logs:
+
+```go
+func main() {
+ // Force log's color
+ gin.ForceConsoleColor()
+
+ // Creates a gin router with default middleware:
+ // logger and recovery (crash-free) middleware
+ router := gin.Default()
+
+ router.GET("/ping", func(c *gin.Context) {
+ c.String(200, "pong")
+ })
+
+ router.Run(":8080")
+}
+```
+
### 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).
@@ -449,10 +619,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
Also, Gin provides two sets of methods for binding:
- **Type** - Must bind
- - **Methods** - `Bind`, `BindJSON`, `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.
- **Type** - Should bind
- - **Methods** - `ShouldBind`, `ShouldBindJSON`, `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.
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`.
@@ -462,8 +632,8 @@ You can also specify that specific fields are required. If a field is decorated
```go
// Binding from JSON
type Login struct {
- User string `form:"user" json:"user" binding:"required"`
- Password string `form:"password" json:"password" binding:"required"`
+ User string `form:"user" json:"user" xml:"user" binding:"required"`
+ Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
func main() {
@@ -472,30 +642,55 @@ func main() {
// Example for binding JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
- if err := c.ShouldBindJSON(&json); err == nil {
- if json.User == "manu" && json.Password == "123" {
- c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
- } else {
- c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
- }
- } else {
+ if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
}
+
+ if json.User != "manu" || json.Password != "123" {
+ c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
+ })
+
+ // Example for binding XML (
+ //
+ //
+ // user
+ // 123
+ // )
+ router.POST("/loginXML", func(c *gin.Context) {
+ var xml Login
+ if err := c.ShouldBindXML(&xml); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ if xml.User != "manu" || xml.Password != "123" {
+ c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// Example for binding a HTML form (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// This will infer what binder to use depending on the content-type header.
- if err := c.ShouldBind(&form); err == nil {
- if form.User == "manu" && form.Password == "123" {
- c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
- } else {
- c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
- }
- } else {
+ if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
}
+
+ if form.User != "manu" || form.Password != "123" {
+ c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// Listen and serve on 0.0.0.0:8080
@@ -525,6 +720,10 @@ $ curl -v -X POST \
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
```
+**Skip validate**
+
+When running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again.
+
### Custom Validators
It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go).
@@ -543,6 +742,7 @@ import (
"gopkg.in/go-playground/validator.v8"
)
+// Booking contains binded and validated data.
type Booking struct {
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"`
@@ -563,7 +763,11 @@ func bookableDate(
func main() {
route := gin.Default()
- binding.Validator.RegisterValidation("bookabledate", bookableDate)
+
+ if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
+ v.RegisterValidation("bookabledate", bookableDate)
+ }
+
route.GET("/bookable", getBookable)
route.Run(":8085")
}
@@ -579,13 +783,16 @@ func getBookable(c *gin.Context) {
```
```console
-$ curl "localhost:8085/bookable?check_in=2017-08-16&check_out=2017-08-17"
+$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
{"message":"Booking dates are valid!"}
-$ curl "localhost:8085/bookable?check_in=2017-08-15&check_out=2017-08-16"
+$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}
```
+[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
+See the [struct-lvl-validation example](examples/struct-lvl-validations) to learn more.
+
### Only Bind Query String
`ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017).
@@ -629,9 +836,12 @@ See the [detail information](https://github.com/gin-gonic/gin/issues/742#issueco
```go
package main
-import "log"
-import "github.com/gin-gonic/gin"
-import "time"
+import (
+ "log"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
type Person struct {
Name string `form:"name"`
@@ -665,6 +875,40 @@ Test it with:
$ 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
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
@@ -696,12 +940,12 @@ form.html
```
@@ -750,7 +994,7 @@ Test it with:
$ curl -v --form user=user --form password=password http://localhost:8080/login
```
-### XML, JSON and YAML rendering
+### XML, JSON, YAML and ProtoBuf rendering
```go
func main() {
@@ -784,6 +1028,19 @@ func main() {
c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
+ r.GET("/someProtoBuf", func(c *gin.Context) {
+ reps := []int64{int64(1), int64(2)}
+ label := "test"
+ // The specific definition of protobuf is written in the testdata/protoexample file.
+ data := &protoexample.Test{
+ Label: &label,
+ Reps: reps,
+ }
+ // Note that data becomes binary data in the response
+ // Will output protoexample.Test protobuf serialized data
+ c.ProtoBuf(http.StatusOK, data)
+ })
+
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
@@ -811,6 +1068,79 @@ func main() {
r.Run(":8080")
}
```
+#### JSONP
+
+Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists.
+
+```go
+func main() {
+ r := gin.Default()
+
+ r.GET("/JSONP?callback=x", func(c *gin.Context) {
+ data := map[string]interface{}{
+ "foo": "bar",
+ }
+
+ //callback is x
+ // Will output : x({\"foo\":\"bar\"})
+ c.JSONP(http.StatusOK, data)
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
+#### AsciiJSON
+
+Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters.
+
+```go
+func main() {
+ r := gin.Default()
+
+ r.GET("/someJSON", func(c *gin.Context) {
+ data := map[string]interface{}{
+ "lang": "GO语言",
+ "tag": " ",
+ }
+
+ // will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
+ c.AsciiJSON(http.StatusOK, data)
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
+#### PureJSON
+
+Normally, JSON replaces special HTML characters with their unicode entities, e.g. `<` becomes `\u003c`. If you want to encode such characters literally, you can use PureJSON instead.
+This feature is unavailable in Go 1.6 and lower.
+
+```go
+func main() {
+ r := gin.Default()
+
+ // Serves unicode entities
+ r.GET("/json", func(c *gin.Context) {
+ c.JSON(200, gin.H{
+ "html": "Hello, world!",
+ })
+ })
+
+ // Serves literal characters
+ r.GET("/purejson", func(c *gin.Context) {
+ c.PureJSON(200, gin.H{
+ "html": "Hello, world!",
+ })
+ })
+
+ // listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
### Serving static files
@@ -826,6 +1156,32 @@ func main() {
}
```
+### Serving data from reader
+
+```go
+func main() {
+ router := gin.Default()
+ router.GET("/someDataFromReader", func(c *gin.Context) {
+ response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png")
+ if err != nil || response.StatusCode != http.StatusOK {
+ c.Status(http.StatusServiceUnavailable)
+ return
+ }
+
+ reader := response.Body
+ contentLength := response.ContentLength
+ contentType := response.Header.Get("Content-Type")
+
+ extraHeaders := map[string]string{
+ "Content-Disposition": `attachment; filename="gopher.png"`,
+ }
+
+ c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
+ })
+ router.Run(":8080")
+}
+```
+
### HTML rendering
Using LoadHTMLGlob() or LoadHTMLFiles()
@@ -920,7 +1276,7 @@ You may use custom delims
```go
r := gin.Default()
r.Delims("{[{", "}]}")
- r.LoadHTMLGlob("/path/to/templates"))
+ r.LoadHTMLGlob("/path/to/templates")
```
#### Custom Template Funcs
@@ -950,7 +1306,7 @@ func main() {
router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
- router.LoadHTMLFiles("./fixtures/basic/raw.tmpl")
+ router.LoadHTMLFiles("./testdata/template/raw.tmpl")
router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
@@ -980,14 +1336,26 @@ Gin allow by default use only one html.Template. Check [a multitemplate render](
### Redirects
-Issuing a HTTP redirect is easy:
+Issuing a HTTP redirect is easy. Both internal and external locations are supported.
```go
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
})
```
-Both internal and external locations are supported.
+
+
+Issuing a Router redirect, use `HandleContext` like below.
+
+``` go
+r.GET("/test", func(c *gin.Context) {
+ c.Request.URL.Path = "/test2"
+ r.HandleContext(c)
+})
+r.GET("/test2", func(c *gin.Context) {
+ c.JSON(200, gin.H{"hello": "world"})
+})
+```
### Custom Middleware
@@ -1304,6 +1672,7 @@ import (
"net/http"
"os"
"os/signal"
+ "syscall"
"time"
"github.com/gin-gonic/gin"
@@ -1323,15 +1692,18 @@ func main() {
go func() {
// service connections
- if err := srv.ListenAndServe(); err != nil {
- log.Printf("listen: %s\n", err)
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatalf("listen: %s\n", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
- signal.Notify(quit, os.Interrupt)
+ // kill (no param) default send syscanll.SIGTERM
+ // kill -2 is syscall.SIGINT
+ // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
@@ -1340,10 +1712,330 @@ func main() {
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
+ // catching ctx.Done(). timeout of 5 seconds.
+ select {
+ case <-ctx.Done():
+ log.Println("timeout of 5 seconds.")
+ }
log.Println("Server exiting")
}
```
+### Build a single binary with templates
+
+You can build a server into a single binary containing templates by using [go-assets][].
+
+[go-assets]: https://github.com/jessevdk/go-assets
+
+```go
+func main() {
+ r := gin.New()
+
+ t, err := loadTemplate()
+ if err != nil {
+ panic(err)
+ }
+ r.SetHTMLTemplate(t)
+
+ r.GET("/", func(c *gin.Context) {
+ c.HTML(http.StatusOK, "/html/index.tmpl",nil)
+ })
+ r.Run(":8080")
+}
+
+// loadTemplate loads templates embedded by go-assets-builder
+func loadTemplate() (*template.Template, error) {
+ t := template.New("")
+ for name, file := range Assets.Files {
+ if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
+ continue
+ }
+ h, err := ioutil.ReadAll(file)
+ if err != nil {
+ return nil, err
+ }
+ t, err = t.New(name).Parse(string(h))
+ if err != nil {
+ return nil, err
+ }
+ }
+ return t, nil
+}
+```
+
+See a complete example in the `examples/assets-in-binary` directory.
+
+### Bind form-data request with custom struct
+
+The follow example using custom struct:
+
+```go
+type StructA struct {
+ FieldA string `form:"field_a"`
+}
+
+type StructB struct {
+ NestedStruct StructA
+ FieldB string `form:"field_b"`
+}
+
+type StructC struct {
+ NestedStructPointer *StructA
+ FieldC string `form:"field_c"`
+}
+
+type StructD struct {
+ NestedAnonyStruct struct {
+ FieldX string `form:"field_x"`
+ }
+ FieldD string `form:"field_d"`
+}
+
+func GetDataB(c *gin.Context) {
+ var b StructB
+ c.Bind(&b)
+ c.JSON(200, gin.H{
+ "a": b.NestedStruct,
+ "b": b.FieldB,
+ })
+}
+
+func GetDataC(c *gin.Context) {
+ var b StructC
+ c.Bind(&b)
+ c.JSON(200, gin.H{
+ "a": b.NestedStructPointer,
+ "c": b.FieldC,
+ })
+}
+
+func GetDataD(c *gin.Context) {
+ var b StructD
+ c.Bind(&b)
+ c.JSON(200, gin.H{
+ "x": b.NestedAnonyStruct,
+ "d": b.FieldD,
+ })
+}
+
+func main() {
+ r := gin.Default()
+ r.GET("/getb", GetDataB)
+ r.GET("/getc", GetDataC)
+ r.GET("/getd", GetDataD)
+
+ r.Run()
+}
+```
+
+Using the command `curl` command result:
+
+```
+$ curl "http://localhost:8080/getb?field_a=hello&field_b=world"
+{"a":{"FieldA":"hello"},"b":"world"}
+$ curl "http://localhost:8080/getc?field_a=hello&field_c=world"
+{"a":{"FieldA":"hello"},"c":"world"}
+$ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
+{"d":"world","x":{"FieldX":"hello"}}
+```
+
+**NOTE**: NOT support the follow style struct:
+
+```go
+type StructX struct {
+ X struct {} `form:"name_x"` // HERE have form
+}
+
+type StructY struct {
+ Y StructX `form:"name_y"` // HERE have form
+}
+
+type StructZ struct {
+ Z *StructZ `form:"name_z"` // HERE have form
+}
+```
+
+In a word, only support nested custom struct which have no `form` now.
+
+### Try to bind body into different structs
+
+The normal methods for binding request body consumes `c.Request.Body` and they
+cannot be called multiple times.
+
+```go
+type formA struct {
+ Foo string `json:"foo" xml:"foo" binding:"required"`
+}
+
+type formB struct {
+ Bar string `json:"bar" xml:"bar" binding:"required"`
+}
+
+func SomeHandler(c *gin.Context) {
+ objA := formA{}
+ objB := formB{}
+ // This c.ShouldBind consumes c.Request.Body and it cannot be reused.
+ if errA := c.ShouldBind(&objA); errA == nil {
+ c.String(http.StatusOK, `the body should be formA`)
+ // Always an error is occurred by this because c.Request.Body is EOF now.
+ } else if errB := c.ShouldBind(&objB); errB == nil {
+ c.String(http.StatusOK, `the body should be formB`)
+ } else {
+ ...
+ }
+}
+```
+
+For this, you can use `c.ShouldBindBodyWith`.
+
+```go
+func SomeHandler(c *gin.Context) {
+ objA := formA{}
+ objB := formB{}
+ // This reads c.Request.Body and stores the result into the context.
+ if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
+ c.String(http.StatusOK, `the body should be formA`)
+ // At this time, it reuses body stored in the context.
+ } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
+ c.String(http.StatusOK, `the body should be formB JSON`)
+ // And it can accepts other formats
+ } else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
+ c.String(http.StatusOK, `the body should be formB XML`)
+ } else {
+ ...
+ }
+}
+```
+
+* `c.ShouldBindBodyWith` stores body into the context before binding. This has
+a slight impact to performance, so you should not use this method if you are
+enough to call binding at once.
+* This feature is only needed for some formats -- `JSON`, `XML`, `MsgPack`,
+`ProtoBuf`. For other formats, `Query`, `Form`, `FormPost`, `FormMultipart`,
+can be called by `c.ShouldBind()` multiple times without any damage to
+performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)).
+
+### http2 server push
+
+http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information.
+
+[embedmd]:# (examples/http-pusher/main.go go)
+```go
+package main
+
+import (
+ "html/template"
+ "log"
+
+ "github.com/gin-gonic/gin"
+)
+
+var html = template.Must(template.New("https").Parse(`
+
+
+ Https Test
+
+
+
+
Welcome, Ginner!
+
+
+`))
+
+func main() {
+ r := gin.Default()
+ r.Static("/assets", "./assets")
+ r.SetHTMLTemplate(html)
+
+ r.GET("/", func(c *gin.Context) {
+ if pusher := c.Writer.Pusher(); pusher != nil {
+ // use pusher.Push() to do server push
+ if err := pusher.Push("/assets/app.js", nil); err != nil {
+ log.Printf("Failed to push: %v", err)
+ }
+ }
+ c.HTML(200, "https", gin.H{
+ "status": "success",
+ })
+ })
+
+ // Listen and Server in https://127.0.0.1:8080
+ r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key")
+}
+```
+
+### 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
The `net/http/httptest` package is preferable way for HTTP testing.
@@ -1390,9 +2082,13 @@ func TestPingRoute(t *testing.T) {
}
```
-## Users [](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
+## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
-* [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.
+* [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.
diff --git a/auth.go b/auth.go
index c2143091..9ed81b5d 100644
--- a/auth.go
+++ b/auth.go
@@ -7,6 +7,7 @@ package gin
import (
"crypto/subtle"
"encoding/base64"
+ "net/http"
"strconv"
)
@@ -51,7 +52,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
if !found {
// Credentials doesn't match, we return 401 and abort handlers chain.
c.Header("WWW-Authenticate", realm)
- c.AbortWithStatus(401)
+ c.AbortWithStatus(http.StatusUnauthorized)
return
}
diff --git a/auth_test.go b/auth_test.go
index dc8523b0..197e9208 100644
--- a/auth_test.go
+++ b/auth_test.go
@@ -93,7 +93,7 @@ func TestBasicAuthSucceed(t *testing.T) {
router := New()
router.Use(BasicAuth(accounts))
router.GET("/login", func(c *Context) {
- c.String(200, c.MustGet(AuthUserKey).(string))
+ c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
})
w := httptest.NewRecorder()
@@ -101,7 +101,7 @@ func TestBasicAuthSucceed(t *testing.T) {
req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req)
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "admin", w.Body.String())
}
@@ -112,7 +112,7 @@ func TestBasicAuth401(t *testing.T) {
router.Use(BasicAuth(accounts))
router.GET("/login", func(c *Context) {
called = true
- c.String(200, c.MustGet(AuthUserKey).(string))
+ c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
})
w := httptest.NewRecorder()
@@ -121,8 +121,8 @@ func TestBasicAuth401(t *testing.T) {
router.ServeHTTP(w, req)
assert.False(t, called)
- assert.Equal(t, 401, w.Code)
- assert.Equal(t, "Basic realm=\"Authorization Required\"", w.HeaderMap.Get("WWW-Authenticate"))
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+ assert.Equal(t, "Basic realm=\"Authorization Required\"", w.Header().Get("WWW-Authenticate"))
}
func TestBasicAuth401WithCustomRealm(t *testing.T) {
@@ -132,7 +132,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\""))
router.GET("/login", func(c *Context) {
called = true
- c.String(200, c.MustGet(AuthUserKey).(string))
+ c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
})
w := httptest.NewRecorder()
@@ -141,6 +141,6 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
router.ServeHTTP(w, req)
assert.False(t, called)
- assert.Equal(t, 401, w.Code)
- assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.HeaderMap.Get("WWW-Authenticate"))
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
+ assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.Header().Get("WWW-Authenticate"))
}
diff --git a/benchmarks_test.go b/benchmarks_test.go
index e7970034..0b3f82df 100644
--- a/benchmarks_test.go
+++ b/benchmarks_test.go
@@ -54,7 +54,7 @@ func BenchmarkOneRouteJSON(B *testing.B) {
Status string `json:"status"`
}{"ok"}
router.GET("/json", func(c *Context) {
- c.JSON(200, data)
+ c.JSON(http.StatusOK, data)
})
runRequest(B, router, "GET", "/json")
}
@@ -66,7 +66,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
router.SetHTMLTemplate(t)
router.GET("/html", func(c *Context) {
- c.HTML(200, "index", "hola")
+ c.HTML(http.StatusOK, "index", "hola")
})
runRequest(B, router, "GET", "/html")
}
@@ -82,7 +82,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
func BenchmarkOneRouteString(B *testing.B) {
router := New()
router.GET("/text", func(c *Context) {
- c.String(200, "this is a plain text")
+ c.String(http.StatusOK, "this is a plain text")
})
runRequest(B, router, "GET", "/text")
}
diff --git a/binding/binding.go b/binding/binding.go
index dc32d538..26d71c9f 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -4,12 +4,9 @@
package binding
-import (
- "net/http"
-
- "gopkg.in/go-playground/validator.v8"
-)
+import "net/http"
+// Content-Type MIME of the most common data formats.
const (
MIMEJSON = "application/json"
MIMEHTML = "text/html"
@@ -21,13 +18,35 @@ const (
MIMEPROTOBUF = "application/x-protobuf"
MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack"
+ MIMEYAML = "application/x-yaml"
)
+// Binding describes the interface which needs to be implemented for binding the
+// data present in the request such as JSON request body, query parameters or
+// the form POST.
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
}
+// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
+// but it reads the body from supplied bytes instead of req.Body.
+type BindingBody interface {
+ Binding
+ 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
+// order for it to be used as the validator engine for ensuring the correctness
+// of the request. Gin provides a default implementation for this using
+// https://github.com/go-playground/validator/tree/v8.18.2.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
@@ -36,14 +55,18 @@ type StructValidator interface {
// Otherwise nil must be returned.
ValidateStruct(interface{}) error
- // RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key
- // NOTE: if the key already exists, the previous validation function will be replaced.
- // NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation
- RegisterValidation(string, validator.Func) error
+ // Engine returns the underlying validator engine which powers the
+ // StructValidator implementation.
+ Engine() interface{}
}
+// Validator is the default validator which implements the StructValidator
+// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
+// under the hood.
var Validator StructValidator = &defaultValidator{}
+// These implement the Binding interface and can be used to bind the data
+// present in the request to struct instances.
var (
JSON = jsonBinding{}
XML = xmlBinding{}
@@ -53,8 +76,12 @@ var (
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
+ YAML = yamlBinding{}
+ Uri = uriBinding{}
)
+// Default returns the appropriate Binding instance based on the HTTP method
+// and the content type.
func Default(method, contentType string) Binding {
if method == "GET" {
return Form
@@ -69,6 +96,8 @@ func Default(method, contentType string) Binding {
return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack
+ case MIMEYAML:
+ return YAML
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
return Form
}
diff --git a/binding/binding_body_test.go b/binding/binding_body_test.go
new file mode 100644
index 00000000..901d429c
--- /dev/null
+++ b/binding/binding_body_test.go
@@ -0,0 +1,72 @@
+package binding
+
+import (
+ "bytes"
+ "io/ioutil"
+ "testing"
+
+ "github.com/gin-gonic/gin/testdata/protoexample"
+ "github.com/golang/protobuf/proto"
+ "github.com/stretchr/testify/assert"
+ "github.com/ugorji/go/codec"
+)
+
+func TestBindingBody(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ binding BindingBody
+ body string
+ want string
+ }{
+ {
+ name: "JSON binding",
+ binding: JSON,
+ body: `{"foo":"FOO"}`,
+ },
+ {
+ name: "XML binding",
+ binding: XML,
+ body: `
+
+ FOO
+`,
+ },
+ {
+ name: "MsgPack binding",
+ binding: MsgPack,
+ body: msgPackBody(t),
+ },
+ {
+ name: "YAML binding",
+ binding: YAML,
+ body: `foo: FOO`,
+ },
+ } {
+ t.Logf("testing: %s", tt.name)
+ req := requestWithBody("POST", "/", tt.body)
+ form := FooStruct{}
+ body, _ := ioutil.ReadAll(req.Body)
+ assert.NoError(t, tt.binding.BindBody(body, &form))
+ assert.Equal(t, FooStruct{"FOO"}, form)
+ }
+}
+
+func msgPackBody(t *testing.T) string {
+ test := FooStruct{"FOO"}
+ h := new(codec.MsgpackHandle)
+ buf := bytes.NewBuffer(nil)
+ assert.NoError(t, codec.NewEncoder(buf, h).Encode(test))
+ return buf.String()
+}
+
+func TestBindingBodyProto(t *testing.T) {
+ test := protoexample.Test{
+ Label: proto.String("FOO"),
+ }
+ data, _ := proto.Marshal(&test)
+ req := requestWithBody("POST", "/", string(data))
+ form := protoexample.Test{}
+ body, _ := ioutil.ReadAll(req.Body)
+ assert.NoError(t, ProtoBuf.BindBody(body, &form))
+ assert.Equal(t, test, form)
+}
diff --git a/binding/binding_test.go b/binding/binding_test.go
index 0c0f9f81..c9dea347 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -11,10 +11,11 @@ import (
"io/ioutil"
"mime/multipart"
"net/http"
+ "strconv"
"testing"
"time"
- "github.com/gin-gonic/gin/binding/example"
+ "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
@@ -29,6 +30,11 @@ type FooBarStruct struct {
Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"`
}
+type FooDefaultBarStruct struct {
+ FooStruct
+ Bar string `msgpack:"bar" json:"bar" form:"bar,default=hello" xml:"bar" binding:"required"`
+}
+
type FooStructUseNumber struct {
Foo interface{} `json:"foo" binding:"required"`
}
@@ -55,6 +61,10 @@ type FooStructForMapType struct {
MapFoo map[string]interface{} `form:"map_foo"`
}
+type FooStructForIgnoreFormTag struct {
+ Foo *string `form:"-"`
+}
+
type InvalidNameType struct {
TestName string `invalid_name:"test_name"`
}
@@ -69,11 +79,27 @@ type FooStructForSliceType struct {
SliceFoo []int `form:"slice_foo"`
}
+type FooStructForStructType struct {
+ StructFoo struct {
+ Idx int `form:"idx"`
+ }
+}
+
+type FooStructForStructPointerType struct {
+ StructPointerFoo *struct {
+ Name string `form:"name"`
+ }
+}
+
type FooStructForSliceMapType struct {
// Unknown type: not support map
SliceMapFoo []map[string]interface{} `form:"slice_map_foo"`
}
+type FooStructForBoolType struct {
+ BoolFoo bool `form:"bool_foo"`
+}
+
type FooBarStructForIntType struct {
IntFoo int `form:"int_foo"`
IntBar int `form:"int_bar" binding:"required"`
@@ -139,27 +165,46 @@ type FooBarStructForFloat64Type struct {
Float64Bar float64 `form:"float64_bar" binding:"required"`
}
+type FooStructForStringPtrType struct {
+ PtrFoo *string `form:"ptr_foo"`
+ PtrBar *string `form:"ptr_bar" binding:"required"`
+}
+
+type FooStructForMapPtrType struct {
+ PtrBar *map[string]interface{} `form:"ptr_bar"`
+}
+
func TestBindingDefault(t *testing.T) {
- assert.Equal(t, Default("GET", ""), Form)
- assert.Equal(t, Default("GET", MIMEJSON), Form)
+ assert.Equal(t, Form, Default("GET", ""))
+ assert.Equal(t, Form, Default("GET", MIMEJSON))
- assert.Equal(t, Default("POST", MIMEJSON), JSON)
- assert.Equal(t, Default("PUT", MIMEJSON), JSON)
+ assert.Equal(t, JSON, Default("POST", MIMEJSON))
+ assert.Equal(t, JSON, Default("PUT", MIMEJSON))
- assert.Equal(t, Default("POST", MIMEXML), XML)
- assert.Equal(t, Default("PUT", MIMEXML2), XML)
+ assert.Equal(t, XML, Default("POST", MIMEXML))
+ assert.Equal(t, XML, Default("PUT", MIMEXML2))
- assert.Equal(t, Default("POST", MIMEPOSTForm), Form)
- assert.Equal(t, Default("PUT", MIMEPOSTForm), Form)
+ assert.Equal(t, Form, Default("POST", MIMEPOSTForm))
+ assert.Equal(t, Form, Default("PUT", MIMEPOSTForm))
- assert.Equal(t, Default("POST", MIMEMultipartPOSTForm), Form)
- assert.Equal(t, Default("PUT", MIMEMultipartPOSTForm), Form)
+ assert.Equal(t, Form, Default("POST", MIMEMultipartPOSTForm))
+ assert.Equal(t, Form, Default("PUT", MIMEMultipartPOSTForm))
- assert.Equal(t, Default("POST", MIMEPROTOBUF), ProtoBuf)
- assert.Equal(t, Default("PUT", MIMEPROTOBUF), ProtoBuf)
+ assert.Equal(t, ProtoBuf, Default("POST", MIMEPROTOBUF))
+ assert.Equal(t, ProtoBuf, Default("PUT", MIMEPROTOBUF))
- assert.Equal(t, Default("POST", MIMEMSGPACK), MsgPack)
- assert.Equal(t, Default("PUT", MIMEMSGPACK2), MsgPack)
+ assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
+ 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) {
@@ -195,6 +240,18 @@ func TestBindingForm2(t *testing.T) {
"", "")
}
+func TestBindingFormDefaultValue(t *testing.T) {
+ testFormBindingDefaultValue(t, "POST",
+ "/", "/",
+ "foo=bar", "bar2=foo")
+}
+
+func TestBindingFormDefaultValue2(t *testing.T) {
+ testFormBindingDefaultValue(t, "GET",
+ "/?foo=bar", "/?bar2=foo",
+ "", "")
+}
+
func TestBindingFormForTime(t *testing.T) {
testFormBindingForTime(t, "POST",
"/", "/",
@@ -225,6 +282,12 @@ func TestBindingFormForTime2(t *testing.T) {
"", "")
}
+func TestFormBindingIgnoreField(t *testing.T) {
+ testFormBindingIgnoreField(t, "POST",
+ "/", "/",
+ "-=bar", "")
+}
+
func TestBindingFormInvalidName(t *testing.T) {
testFormBindingInvalidName(t, "POST",
"/", "/",
@@ -361,6 +424,30 @@ func TestBindingFormForType(t *testing.T) {
testFormBindingForType(t, "GET",
"/?float64_foo=&float64_bar=-12.34", "/?bar2=12.3",
"", "", "Float64")
+
+ testFormBindingForType(t, "POST",
+ "/", "/",
+ "ptr_bar=test", "bar2=test", "Ptr")
+
+ testFormBindingForType(t, "GET",
+ "/?ptr_bar=test", "/?bar2=test",
+ "", "", "Ptr")
+
+ testFormBindingForType(t, "POST",
+ "/", "/",
+ "idx=123", "id1=1", "Struct")
+
+ testFormBindingForType(t, "GET",
+ "/?idx=123", "/?id1=1",
+ "", "", "Struct")
+
+ testFormBindingForType(t, "POST",
+ "/", "/",
+ "name=thinkerou", "name1=ou", "StructPointer")
+
+ testFormBindingForType(t, "GET",
+ "/?name=thinkerou", "/?name1=ou",
+ "", "", "StructPointer")
}
func TestBindingQuery(t *testing.T) {
@@ -387,6 +474,12 @@ func TestBindingQueryFail2(t *testing.T) {
"map_foo=unused", "")
}
+func TestBindingQueryBoolFail(t *testing.T) {
+ testQueryBindingBoolFail(t, "GET",
+ "/?bool_foo=fasl", "/?bar2=foo",
+ "bool_foo=unused", "")
+}
+
func TestBindingXML(t *testing.T) {
testBodyBinding(t,
XML, "xml",
@@ -401,40 +494,60 @@ func TestBindingXMLFail(t *testing.T) {
"", "")
}
+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 {
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
+func createDefaultFormPostRequest() *http.Request {
+ req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar"))
+ req.Header.Set("Content-Type", MIMEPOSTForm)
+ return req
+}
+
func createFormPostRequestFail() *http.Request {
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", bytes.NewBufferString("map_foo=bar"))
req.Header.Set("Content-Type", MIMEPOSTForm)
return req
}
-func createFormMultipartRequest() *http.Request {
+func createFormMultipartRequest(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
- mw.SetBoundary(boundary)
- mw.WriteField("foo", "bar")
- mw.WriteField("bar", "foo")
+ assert.NoError(t, mw.SetBoundary(boundary))
+ assert.NoError(t, mw.WriteField("foo", "bar"))
+ assert.NoError(t, mw.WriteField("bar", "foo"))
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
-func createFormMultipartRequestFail() *http.Request {
+func createFormMultipartRequestFail(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
- mw.SetBoundary(boundary)
- mw.WriteField("map_foo", "bar")
+ assert.NoError(t, mw.SetBoundary(boundary))
+ assert.NoError(t, mw.WriteField("map_foo", "bar"))
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
@@ -443,11 +556,20 @@ func createFormMultipartRequestFail() *http.Request {
func TestBindingFormPost(t *testing.T) {
req := createFormPostRequest()
var obj FooBarStruct
- FormPost.Bind(req, &obj)
+ assert.NoError(t, FormPost.Bind(req, &obj))
- assert.Equal(t, FormPost.Name(), "form-urlencoded")
- assert.Equal(t, obj.Foo, "bar")
- assert.Equal(t, obj.Bar, "foo")
+ assert.Equal(t, "form-urlencoded", FormPost.Name())
+ assert.Equal(t, "bar", obj.Foo)
+ assert.Equal(t, "foo", obj.Bar)
+}
+
+func TestBindingDefaultValueFormPost(t *testing.T) {
+ req := createDefaultFormPostRequest()
+ var obj FooDefaultBarStruct
+ assert.NoError(t, FormPost.Bind(req, &obj))
+
+ assert.Equal(t, "bar", obj.Foo)
+ assert.Equal(t, "hello", obj.Bar)
}
func TestBindingFormPostFail(t *testing.T) {
@@ -458,24 +580,24 @@ func TestBindingFormPostFail(t *testing.T) {
}
func TestBindingFormMultipart(t *testing.T) {
- req := createFormMultipartRequest()
+ req := createFormMultipartRequest(t)
var obj FooBarStruct
- FormMultipart.Bind(req, &obj)
+ assert.NoError(t, FormMultipart.Bind(req, &obj))
- assert.Equal(t, FormMultipart.Name(), "multipart/form-data")
- assert.Equal(t, obj.Foo, "bar")
- assert.Equal(t, obj.Bar, "foo")
+ assert.Equal(t, "multipart/form-data", FormMultipart.Name())
+ assert.Equal(t, "bar", obj.Foo)
+ assert.Equal(t, "foo", obj.Bar)
}
func TestBindingFormMultipartFail(t *testing.T) {
- req := createFormMultipartRequestFail()
+ req := createFormMultipartRequestFail(t)
var obj FooStructForMapType
err := FormMultipart.Bind(req, &obj)
assert.Error(t, err)
}
func TestBindingProtoBuf(t *testing.T) {
- test := &example.Test{
+ test := &protoexample.Test{
Label: proto.String("yes"),
}
data, _ := proto.Marshal(test)
@@ -487,7 +609,7 @@ func TestBindingProtoBuf(t *testing.T) {
}
func TestBindingProtoBufFail(t *testing.T) {
- test := &example.Test{
+ test := &protoexample.Test{
Label: proto.String("yes"),
}
data, _ := proto.Marshal(test)
@@ -558,9 +680,52 @@ func TestExistsFails(t *testing.T) {
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, ¬))
+ 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) {
b := Form
- assert.Equal(t, b.Name(), "form")
+ assert.Equal(t, "form", b.Name())
obj := FooBarStruct{}
req := requestWithBody(method, path, body)
@@ -569,8 +734,8 @@ func testFormBinding(t *testing.T, method, path, badPath, body, badBody string)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Foo, "bar")
- assert.Equal(t, obj.Bar, "foo")
+ assert.Equal(t, "bar", obj.Foo)
+ assert.Equal(t, "foo", obj.Bar)
obj = FooBarStruct{}
req = requestWithBody(method, badPath, badBody)
@@ -578,9 +743,29 @@ func testFormBinding(t *testing.T, method, path, badPath, body, badBody string)
assert.Error(t, err)
}
+func testFormBindingDefaultValue(t *testing.T, method, path, badPath, body, badBody string) {
+ b := Form
+ assert.Equal(t, "form", b.Name())
+
+ obj := FooDefaultBarStruct{}
+ req := requestWithBody(method, path, body)
+ if method == "POST" {
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ }
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.Equal(t, "bar", obj.Foo)
+ assert.Equal(t, "hello", obj.Bar)
+
+ obj = FooDefaultBarStruct{}
+ req = requestWithBody(method, badPath, badBody)
+ err = JSON.Bind(req, &obj)
+ assert.Error(t, err)
+}
+
func TestFormBindingFail(t *testing.T) {
b := Form
- assert.Equal(t, b.Name(), "form")
+ assert.Equal(t, "form", b.Name())
obj := FooBarStruct{}
req, _ := http.NewRequest("POST", "/", nil)
@@ -590,7 +775,7 @@ func TestFormBindingFail(t *testing.T) {
func TestFormPostBindingFail(t *testing.T) {
b := FormPost
- assert.Equal(t, b.Name(), "form-urlencoded")
+ assert.Equal(t, "form-urlencoded", b.Name())
obj := FooBarStruct{}
req, _ := http.NewRequest("POST", "/", nil)
@@ -600,7 +785,7 @@ func TestFormPostBindingFail(t *testing.T) {
func TestFormMultipartBindingFail(t *testing.T) {
b := FormMultipart
- assert.Equal(t, b.Name(), "multipart/form-data")
+ assert.Equal(t, "multipart/form-data", b.Name())
obj := FooBarStruct{}
req, _ := http.NewRequest("POST", "/", nil)
@@ -610,7 +795,7 @@ func TestFormMultipartBindingFail(t *testing.T) {
func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
- assert.Equal(t, b.Name(), "form")
+ assert.Equal(t, "form", b.Name())
obj := FooBarStructForTimeType{}
req := requestWithBody(method, path, body)
@@ -620,10 +805,10 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.TimeFoo.Unix(), int64(1510675200))
- assert.Equal(t, obj.TimeFoo.Location().String(), "Asia/Chongqing")
- assert.Equal(t, obj.TimeBar.Unix(), int64(-62135596800))
- assert.Equal(t, obj.TimeBar.Location().String(), "UTC")
+ assert.Equal(t, int64(1510675200), obj.TimeFoo.Unix())
+ assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String())
+ assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
+ assert.Equal(t, "UTC", obj.TimeBar.Location().String())
obj = FooBarStructForTimeType{}
req = requestWithBody(method, badPath, badBody)
@@ -633,7 +818,7 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
- assert.Equal(t, b.Name(), "form")
+ assert.Equal(t, "form", b.Name())
obj := FooStructForTimeTypeNotFormat{}
req := requestWithBody(method, path, body)
@@ -651,7 +836,7 @@ func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body,
func testFormBindingForTimeFailFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
- assert.Equal(t, b.Name(), "form")
+ assert.Equal(t, "form", b.Name())
obj := FooStructForTimeTypeFailFormat{}
req := requestWithBody(method, path, body)
@@ -669,7 +854,7 @@ func testFormBindingForTimeFailFormat(t *testing.T, method, path, badPath, body,
func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
- assert.Equal(t, b.Name(), "form")
+ assert.Equal(t, "form", b.Name())
obj := FooStructForTimeTypeFailLocation{}
req := requestWithBody(method, path, body)
@@ -685,9 +870,24 @@ func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, bod
assert.Error(t, err)
}
+func testFormBindingIgnoreField(t *testing.T, method, path, badPath, body, badBody string) {
+ b := Form
+ assert.Equal(t, "form", b.Name())
+
+ obj := FooStructForIgnoreFormTag{}
+ req := requestWithBody(method, path, body)
+ if method == "POST" {
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ }
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+
+ assert.Nil(t, obj.Foo)
+}
+
func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
- assert.Equal(t, b.Name(), "form")
+ assert.Equal(t, "form", b.Name())
obj := InvalidNameType{}
req := requestWithBody(method, path, body)
@@ -696,7 +896,7 @@ func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBo
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.TestName, "")
+ assert.Equal(t, "", obj.TestName)
obj = InvalidNameType{}
req = requestWithBody(method, badPath, badBody)
@@ -706,7 +906,7 @@ func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBo
func testFormBindingInvalidName2(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
- assert.Equal(t, b.Name(), "form")
+ assert.Equal(t, "form", b.Name())
obj := InvalidNameMapType{}
req := requestWithBody(method, path, body)
@@ -724,7 +924,7 @@ func testFormBindingInvalidName2(t *testing.T, method, path, badPath, body, badB
func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody string, typ string) {
b := Form
- assert.Equal(t, b.Name(), "form")
+ assert.Equal(t, "form", b.Name())
req := requestWithBody(method, path, body)
if method == "POST" {
@@ -735,8 +935,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForIntType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.IntFoo, int(0))
- assert.Equal(t, obj.IntBar, int(-12))
+ assert.Equal(t, int(0), obj.IntFoo)
+ assert.Equal(t, int(-12), obj.IntBar)
obj = FooBarStructForIntType{}
req = requestWithBody(method, badPath, badBody)
@@ -746,8 +946,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForInt8Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Int8Foo, int8(0))
- assert.Equal(t, obj.Int8Bar, int8(-12))
+ assert.Equal(t, int8(0), obj.Int8Foo)
+ assert.Equal(t, int8(-12), obj.Int8Bar)
obj = FooBarStructForInt8Type{}
req = requestWithBody(method, badPath, badBody)
@@ -757,8 +957,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForInt16Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Int16Foo, int16(0))
- assert.Equal(t, obj.Int16Bar, int16(-12))
+ assert.Equal(t, int16(0), obj.Int16Foo)
+ assert.Equal(t, int16(-12), obj.Int16Bar)
obj = FooBarStructForInt16Type{}
req = requestWithBody(method, badPath, badBody)
@@ -768,8 +968,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForInt32Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Int32Foo, int32(0))
- assert.Equal(t, obj.Int32Bar, int32(-12))
+ assert.Equal(t, int32(0), obj.Int32Foo)
+ assert.Equal(t, int32(-12), obj.Int32Bar)
obj = FooBarStructForInt32Type{}
req = requestWithBody(method, badPath, badBody)
@@ -779,8 +979,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForInt64Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Int64Foo, int64(0))
- assert.Equal(t, obj.Int64Bar, int64(-12))
+ assert.Equal(t, int64(0), obj.Int64Foo)
+ assert.Equal(t, int64(-12), obj.Int64Bar)
obj = FooBarStructForInt64Type{}
req = requestWithBody(method, badPath, badBody)
@@ -790,8 +990,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForUintType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.UintFoo, uint(0x0))
- assert.Equal(t, obj.UintBar, uint(0xc))
+ assert.Equal(t, uint(0x0), obj.UintFoo)
+ assert.Equal(t, uint(0xc), obj.UintBar)
obj = FooBarStructForUintType{}
req = requestWithBody(method, badPath, badBody)
@@ -801,8 +1001,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForUint8Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Uint8Foo, uint8(0x0))
- assert.Equal(t, obj.Uint8Bar, uint8(0xc))
+ assert.Equal(t, uint8(0x0), obj.Uint8Foo)
+ assert.Equal(t, uint8(0xc), obj.Uint8Bar)
obj = FooBarStructForUint8Type{}
req = requestWithBody(method, badPath, badBody)
@@ -812,8 +1012,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForUint16Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Uint16Foo, uint16(0x0))
- assert.Equal(t, obj.Uint16Bar, uint16(0xc))
+ assert.Equal(t, uint16(0x0), obj.Uint16Foo)
+ assert.Equal(t, uint16(0xc), obj.Uint16Bar)
obj = FooBarStructForUint16Type{}
req = requestWithBody(method, badPath, badBody)
@@ -823,8 +1023,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForUint32Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Uint32Foo, uint32(0x0))
- assert.Equal(t, obj.Uint32Bar, uint32(0xc))
+ assert.Equal(t, uint32(0x0), obj.Uint32Foo)
+ assert.Equal(t, uint32(0xc), obj.Uint32Bar)
obj = FooBarStructForUint32Type{}
req = requestWithBody(method, badPath, badBody)
@@ -834,8 +1034,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForUint64Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Uint64Foo, uint64(0x0))
- assert.Equal(t, obj.Uint64Bar, uint64(0xc))
+ assert.Equal(t, uint64(0x0), obj.Uint64Foo)
+ assert.Equal(t, uint64(0xc), obj.Uint64Bar)
obj = FooBarStructForUint64Type{}
req = requestWithBody(method, badPath, badBody)
@@ -845,8 +1045,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForFloat32Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Float32Foo, float32(0.0))
- assert.Equal(t, obj.Float32Bar, float32(-12.34))
+ assert.Equal(t, float32(0.0), obj.Float32Foo)
+ assert.Equal(t, float32(-12.34), obj.Float32Bar)
obj = FooBarStructForFloat32Type{}
req = requestWithBody(method, badPath, badBody)
@@ -856,8 +1056,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForFloat64Type{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Float64Foo, float64(0.0))
- assert.Equal(t, obj.Float64Bar, float64(-12.34))
+ assert.Equal(t, float64(0.0), obj.Float64Foo)
+ assert.Equal(t, float64(-12.34), obj.Float64Bar)
obj = FooBarStructForFloat64Type{}
req = requestWithBody(method, badPath, badBody)
@@ -867,8 +1067,8 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooBarStructForBoolType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.BoolFoo, false)
- assert.Equal(t, obj.BoolBar, true)
+ assert.False(t, obj.BoolFoo)
+ assert.True(t, obj.BoolBar)
obj = FooBarStructForBoolType{}
req = requestWithBody(method, badPath, badBody)
@@ -878,12 +1078,34 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooStructForSliceType{}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.SliceFoo, []int{1, 2})
+ assert.Equal(t, []int{1, 2}, obj.SliceFoo)
obj = FooStructForSliceType{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
+ case "Struct":
+ obj := FooStructForStructType{}
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.Equal(t,
+ struct {
+ Idx int "form:\"idx\""
+ }(struct {
+ Idx int "form:\"idx\""
+ }{Idx: 123}),
+ obj.StructFoo)
+ case "StructPointer":
+ obj := FooStructForStructPointerType{}
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.Equal(t,
+ struct {
+ Name string "form:\"name\""
+ }(struct {
+ Name string "form:\"name\""
+ }{Name: "thinkerou"}),
+ *obj.StructPointerFoo)
case "Map":
obj := FooStructForMapType{}
err := b.Bind(req, &obj)
@@ -892,12 +1114,33 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s
obj := FooStructForSliceMapType{}
err := b.Bind(req, &obj)
assert.Error(t, err)
+ case "Ptr":
+ obj := FooStructForStringPtrType{}
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.Nil(t, obj.PtrFoo)
+ assert.Equal(t, "test", *obj.PtrBar)
+
+ obj = FooStructForStringPtrType{}
+ obj.PtrBar = new(string)
+ err = b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.Equal(t, "test", *obj.PtrBar)
+
+ objErr := FooStructForMapPtrType{}
+ err = b.Bind(req, &objErr)
+ assert.Error(t, err)
+
+ obj = FooStructForStringPtrType{}
+ req = requestWithBody(method, badPath, badBody)
+ err = b.Bind(req, &obj)
+ assert.Error(t, err)
}
}
func testQueryBinding(t *testing.T, method, path, badPath, body, badBody string) {
b := Query
- assert.Equal(t, b.Name(), "query")
+ assert.Equal(t, "query", b.Name())
obj := FooBarStruct{}
req := requestWithBody(method, path, body)
@@ -906,13 +1149,13 @@ func testQueryBinding(t *testing.T, method, path, badPath, body, badBody string)
}
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Foo, "bar")
- assert.Equal(t, obj.Bar, "foo")
+ assert.Equal(t, "bar", obj.Foo)
+ assert.Equal(t, "foo", obj.Bar)
}
func testQueryBindingFail(t *testing.T, method, path, badPath, body, badBody string) {
b := Query
- assert.Equal(t, b.Name(), "query")
+ assert.Equal(t, "query", b.Name())
obj := FooStructForMapType{}
req := requestWithBody(method, path, body)
@@ -923,14 +1166,27 @@ func testQueryBindingFail(t *testing.T, method, path, badPath, body, badBody str
assert.Error(t, err)
}
+func testQueryBindingBoolFail(t *testing.T, method, path, badPath, body, badBody string) {
+ b := Query
+ assert.Equal(t, "query", b.Name())
+
+ obj := FooStructForBoolType{}
+ req := requestWithBody(method, path, body)
+ if method == "POST" {
+ req.Header.Add("Content-Type", MIMEPOSTForm)
+ }
+ err := b.Bind(req, &obj)
+ assert.Error(t, err)
+}
+
func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
- assert.Equal(t, b.Name(), name)
+ assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Foo, "bar")
+ assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
@@ -939,7 +1195,7 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody
}
func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
- assert.Equal(t, b.Name(), name)
+ assert.Equal(t, name, b.Name())
obj := FooStructUseNumber{}
req := requestWithBody("POST", path, body)
@@ -949,7 +1205,7 @@ func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body
// we hope it is int64(123)
v, e := obj.Foo.(json.Number).Int64()
assert.NoError(t, e)
- assert.Equal(t, v, int64(123))
+ assert.Equal(t, int64(123), v)
obj = FooStructUseNumber{}
req = requestWithBody("POST", badPath, badBody)
@@ -958,7 +1214,7 @@ func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body
}
func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
- assert.Equal(t, b.Name(), name)
+ assert.Equal(t, name, b.Name())
obj := FooStructUseNumber{}
req := requestWithBody("POST", path, body)
@@ -967,7 +1223,7 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
assert.NoError(t, err)
// it will return float64(123) if not use EnableDecoderUseNumber
// maybe it is not hoped
- assert.Equal(t, obj.Foo, float64(123))
+ assert.Equal(t, float64(123), obj.Foo)
obj = FooStructUseNumber{}
req = requestWithBody("POST", badPath, badBody)
@@ -976,13 +1232,13 @@ func testBodyBindingUseNumber2(t *testing.T, b Binding, name, path, badPath, bod
}
func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
- assert.Equal(t, b.Name(), name)
+ assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
err := b.Bind(req, &obj)
assert.Error(t, err)
- assert.Equal(t, obj.Foo, "")
+ assert.Equal(t, "", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
@@ -991,16 +1247,16 @@ func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, bad
}
func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
- assert.Equal(t, b.Name(), name)
+ assert.Equal(t, name, b.Name())
- obj := example.Test{}
+ obj := protoexample.Test{}
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEPROTOBUF)
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, *obj.Label, "yes")
+ assert.Equal(t, "yes", *obj.Label)
- obj = example.Test{}
+ obj = protoexample.Test{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF)
err = ProtoBuf.Bind(req, &obj)
@@ -1014,9 +1270,9 @@ func (h hook) Read([]byte) (int, error) {
}
func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
- assert.Equal(t, b.Name(), name)
+ assert.Equal(t, name, b.Name())
- obj := example.Test{}
+ obj := protoexample.Test{}
req := requestWithBody("POST", path, body)
req.Body = ioutil.NopCloser(&hook{})
@@ -1024,7 +1280,7 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
err := b.Bind(req, &obj)
assert.Error(t, err)
- obj = example.Test{}
+ obj = protoexample.Test{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF)
err = ProtoBuf.Bind(req, &obj)
@@ -1032,14 +1288,14 @@ func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body
}
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
- assert.Equal(t, b.Name(), name)
+ assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
assert.NoError(t, err)
- assert.Equal(t, obj.Foo, "bar")
+ assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
@@ -1052,3 +1308,12 @@ func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return
}
+
+func TestCanSet(t *testing.T) {
+ type CanSetStruct struct {
+ lowerStart string `form:"lower"`
+ }
+
+ var c CanSetStruct
+ assert.Nil(t, mapForm(&c, nil))
+}
diff --git a/binding/default_validator.go b/binding/default_validator.go
index 6336bb6e..e7a302de 100644
--- a/binding/default_validator.go
+++ b/binding/default_validator.go
@@ -18,19 +18,29 @@ type defaultValidator struct {
var _ StructValidator = &defaultValidator{}
+// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
- if kindOfData(obj) == reflect.Struct {
+ value := reflect.ValueOf(obj)
+ valueType := value.Kind()
+ if valueType == reflect.Ptr {
+ valueType = value.Elem().Kind()
+ }
+ if valueType == reflect.Struct {
v.lazyinit()
if err := v.validate.Struct(obj); err != nil {
- return error(err)
+ return err
}
}
return nil
}
-func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) error {
+// Engine returns the underlying validator engine which powers the default
+// Validator instance. This is useful if you want to register custom validations
+// or struct level validations. See validator GoDoc for more info -
+// https://godoc.org/gopkg.in/go-playground/validator.v8
+func (v *defaultValidator) Engine() interface{} {
v.lazyinit()
- return v.validate.RegisterValidation(key, fn)
+ return v.validate
}
func (v *defaultValidator) lazyinit() {
@@ -39,12 +49,3 @@ func (v *defaultValidator) lazyinit() {
v.validate = validator.New(config)
})
}
-
-func kindOfData(data interface{}) reflect.Kind {
- value := reflect.ValueOf(data)
- valueType := value.Kind()
- if valueType == reflect.Ptr {
- valueType = value.Elem().Kind()
- }
- return valueType
-}
diff --git a/binding/form.go b/binding/form.go
index 0be59660..8955c95b 100644
--- a/binding/form.go
+++ b/binding/form.go
@@ -20,7 +20,11 @@ func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil {
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 {
return err
}
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index dd8c6246..8eb5c0d1 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -8,10 +8,19 @@ import (
"errors"
"reflect"
"strconv"
+ "strings"
"time"
)
+func mapUri(ptr interface{}, m map[string][]string) error {
+ return mapFormByTag(ptr, m, "uri")
+}
+
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()
val := reflect.ValueOf(ptr).Elem()
for i := 0; i < typ.NumField(); i++ {
@@ -22,15 +31,34 @@ func mapForm(ptr interface{}, form map[string][]string) error {
}
structFieldKind := structField.Kind()
- inputFieldName := typeField.Tag.Get("form")
+ inputFieldName := typeField.Tag.Get(tag)
+ inputFieldNameList := strings.Split(inputFieldName, ",")
+ inputFieldName = inputFieldNameList[0]
+ var defaultValue string
+ if len(inputFieldNameList) > 1 {
+ defaultList := strings.SplitN(inputFieldNameList[1], "=", 2)
+ if defaultList[0] == "default" {
+ defaultValue = defaultList[1]
+ }
+ }
+ if inputFieldName == "-" {
+ continue
+ }
if inputFieldName == "" {
inputFieldName = typeField.Name
- // if "form" tag is nil, we inspect if the field is a struct.
+ // if "form" tag is nil, we inspect if the field is a struct or struct pointer.
// this would not make sense for JSON parsing but it does for a form
// since data is flatten
+ if structFieldKind == reflect.Ptr {
+ if !structField.Elem().IsValid() {
+ structField.Set(reflect.New(structField.Type().Elem()))
+ }
+ structField = structField.Elem()
+ structFieldKind = structField.Kind()
+ }
if structFieldKind == reflect.Struct {
- err := mapForm(structField.Addr().Interface(), form)
+ err := mapFormByTag(structField.Addr().Interface(), form, tag)
if err != nil {
return err
}
@@ -38,8 +66,13 @@ func mapForm(ptr interface{}, form map[string][]string) error {
}
}
inputValue, exists := form[inputFieldName]
+
if !exists {
- continue
+ if defaultValue == "" {
+ continue
+ }
+ inputValue = make([]string, 1)
+ inputValue[0] = defaultValue
}
numElems := len(inputValue)
@@ -52,16 +85,16 @@ func mapForm(ptr interface{}, form map[string][]string) error {
}
}
val.Field(i).Set(slice)
- } else {
- if _, isTime := structField.Interface().(time.Time); isTime {
- if err := setTimeField(inputValue[0], typeField, structField); err != nil {
- return err
- }
- continue
- }
- if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
+ continue
+ }
+ if _, isTime := structField.Interface().(time.Time); isTime {
+ if err := setTimeField(inputValue[0], typeField, structField); err != nil {
return err
}
+ continue
+ }
+ if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
+ return err
}
}
return nil
@@ -97,6 +130,12 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V
return setFloatField(val, 64, structField)
case reflect.String:
structField.SetString(val)
+ case reflect.Ptr:
+ if !structField.Elem().IsValid() {
+ structField.Set(reflect.New(structField.Type().Elem()))
+ }
+ structFieldElem := structField.Elem()
+ return setWithProperType(structFieldElem.Kind(), val, structFieldElem)
default:
return errors.New("Unknown type")
}
@@ -133,7 +172,7 @@ func setBoolField(val string, field reflect.Value) error {
if err == nil {
field.SetBool(boolVal)
}
- return nil
+ return err
}
func setFloatField(val string, bitSize int, field reflect.Value) error {
@@ -150,7 +189,7 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
timeFormat := structField.Tag.Get("time_format")
if timeFormat == "" {
- return errors.New("Blank time format")
+ timeFormat = time.RFC3339
}
if val == "" {
diff --git a/binding/json.go b/binding/json.go
index b7c856af..f968161b 100644
--- a/binding/json.go
+++ b/binding/json.go
@@ -5,11 +5,17 @@
package binding
import (
+ "bytes"
+ "fmt"
+ "io"
"net/http"
- "github.com/gin-gonic/gin/json"
+ "github.com/gin-gonic/gin/internal/json"
)
+// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
+// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
+// interface{} as a Number instead of as a float64.
var EnableDecoderUseNumber = false
type jsonBinding struct{}
@@ -19,7 +25,18 @@ func (jsonBinding) Name() string {
}
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
- decoder := json.NewDecoder(req.Body)
+ if req == nil || req.Body == nil {
+ return fmt.Errorf("invalid request")
+ }
+ return decodeJSON(req.Body, obj)
+}
+
+func (jsonBinding) BindBody(body []byte, obj interface{}) error {
+ return decodeJSON(bytes.NewReader(body), obj)
+}
+
+func decodeJSON(r io.Reader, obj interface{}) error {
+ decoder := json.NewDecoder(r)
if EnableDecoderUseNumber {
decoder.UseNumber()
}
diff --git a/binding/msgpack.go b/binding/msgpack.go
index 7faea4b5..b7f73197 100644
--- a/binding/msgpack.go
+++ b/binding/msgpack.go
@@ -5,6 +5,8 @@
package binding
import (
+ "bytes"
+ "io"
"net/http"
"github.com/ugorji/go/codec"
@@ -17,7 +19,16 @@ func (msgpackBinding) Name() string {
}
func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
- if err := codec.NewDecoder(req.Body, new(codec.MsgpackHandle)).Decode(&obj); err != nil {
+ return decodeMsgPack(req.Body, obj)
+}
+
+func (msgpackBinding) BindBody(body []byte, obj interface{}) error {
+ return decodeMsgPack(bytes.NewReader(body), obj)
+}
+
+func decodeMsgPack(r io.Reader, obj interface{}) error {
+ cdc := new(codec.MsgpackHandle)
+ if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
return err
}
return validate(obj)
diff --git a/binding/protobuf.go b/binding/protobuf.go
index c7eb84e9..f9ece928 100644
--- a/binding/protobuf.go
+++ b/binding/protobuf.go
@@ -17,19 +17,20 @@ func (protobufBinding) Name() string {
return "protobuf"
}
-func (protobufBinding) Bind(req *http.Request, obj interface{}) error {
-
+func (b protobufBinding) Bind(req *http.Request, obj interface{}) error {
buf, err := ioutil.ReadAll(req.Body)
if err != nil {
return err
}
+ return b.BindBody(buf, obj)
+}
- if err = proto.Unmarshal(buf, obj.(proto.Message)); err != nil {
+func (protobufBinding) BindBody(body []byte, obj interface{}) error {
+ if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
return err
}
-
- //Here it's same to return validate(obj), but util now we cann't add `binding:""` to the struct
- //which automatically generate by gen-proto
+ // 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
return nil
- //return validate(obj)
+ // return validate(obj)
}
diff --git a/binding/uri.go b/binding/uri.go
new file mode 100644
index 00000000..f91ec381
--- /dev/null
+++ b/binding/uri.go
@@ -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)
+}
diff --git a/binding/validate_test.go b/binding/validate_test.go
index 8ca79989..2c76b6d6 100644
--- a/binding/validate_test.go
+++ b/binding/validate_test.go
@@ -176,7 +176,7 @@ func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1}
assert.NoError(t, validate(obj))
assert.NoError(t, validate(&obj))
- assert.Equal(t, obj, Object{"foo": "bar", "bar": 1})
+ assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj)
obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}}
assert.NoError(t, validate(obj2))
@@ -185,12 +185,12 @@ func TestValidatePrimitives(t *testing.T) {
nu := 10
assert.NoError(t, validate(nu))
assert.NoError(t, validate(&nu))
- assert.Equal(t, nu, 10)
+ assert.Equal(t, 10, nu)
str := "value"
assert.NoError(t, validate(str))
assert.NoError(t, validate(&str))
- assert.Equal(t, str, "value")
+ assert.Equal(t, "value", str)
}
// structCustomValidation is a helper struct we use to check that
@@ -214,11 +214,14 @@ func notOne(
return false
}
-func TestRegisterValidation(t *testing.T) {
+func TestValidatorEngine(t *testing.T) {
// This validates that the function `notOne` matches
// the expected function signature by `defaultValidator`
// and by extension the validator library.
- err := Validator.RegisterValidation("notone", notOne)
+ engine, ok := Validator.Engine().(*validator.Validate)
+ assert.True(t, ok)
+
+ err := engine.RegisterValidation("notone", notOne)
// Check that we can register custom validation without error
assert.Nil(t, err)
@@ -228,6 +231,6 @@ func TestRegisterValidation(t *testing.T) {
// Check that we got back non-nil errs
assert.NotNil(t, errs)
- // Check that the error matches expactation
+ // Check that the error matches expectation
assert.Error(t, errs, "", "", "notone")
}
diff --git a/binding/xml.go b/binding/xml.go
index f84a6b7f..4e901149 100644
--- a/binding/xml.go
+++ b/binding/xml.go
@@ -5,7 +5,9 @@
package binding
import (
+ "bytes"
"encoding/xml"
+ "io"
"net/http"
)
@@ -16,7 +18,14 @@ func (xmlBinding) Name() string {
}
func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
- decoder := xml.NewDecoder(req.Body)
+ return decodeXML(req.Body, obj)
+}
+
+func (xmlBinding) BindBody(body []byte, obj interface{}) error {
+ return decodeXML(bytes.NewReader(body), obj)
+}
+func decodeXML(r io.Reader, obj interface{}) error {
+ decoder := xml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
diff --git a/binding/yaml.go b/binding/yaml.go
new file mode 100644
index 00000000..a2d36d6a
--- /dev/null
+++ b/binding/yaml.go
@@ -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)
+}
diff --git a/context.go b/context.go
index 90d4c6e5..7618cef5 100644
--- a/context.go
+++ b/context.go
@@ -31,6 +31,8 @@ const (
MIMEPlain = binding.MIMEPlain
MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
+ MIMEYAML = binding.MIMEYAML
+ BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
)
const abortIndex int8 = math.MaxInt8 / 2
@@ -80,6 +82,10 @@ func (c *Context) Copy() *Context {
cp.Writer = &cp.writermem
cp.index = abortIndex
cp.handlers = nil
+ cp.Keys = map[string]interface{}{}
+ for k, v := range c.Keys {
+ cp.Keys[k] = v
+ }
return &cp
}
@@ -89,6 +95,16 @@ func (c *Context) HandlerName() string {
return nameOfFunction(c.handlers.Last())
}
+// HandlerNames returns a list of all registered handlers for this context in descending order,
+// following the semantics of HandlerName()
+func (c *Context) HandlerNames() []string {
+ hn := make([]string, 0, len(c.handlers))
+ for _, val := range c.handlers {
+ hn = append(hn, nameOfFunction(val))
+ }
+ return hn
+}
+
// Handler returns the main handler.
func (c *Context) Handler() HandlerFunc {
return c.handlers.Last()
@@ -103,8 +119,9 @@ func (c *Context) Handler() HandlerFunc {
// See example in GitHub.
func (c *Context) Next() {
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.index++
}
}
@@ -158,16 +175,15 @@ func (c *Context) Error(err error) *Error {
if err == nil {
panic("err is nil")
}
- var parsedError *Error
- switch err.(type) {
- case *Error:
- parsedError = err.(*Error)
- default:
+
+ parsedError, ok := err.(*Error)
+ if !ok {
parsedError = &Error{
Err: err,
Type: ErrorTypePrivate,
}
}
+
c.Errors = append(c.Errors, parsedError)
return parsedError
}
@@ -360,6 +376,18 @@ func (c *Context) GetQueryArray(key string) ([]string, bool) {
return []string{}, false
}
+// QueryMap returns a map for a given query key.
+func (c *Context) QueryMap(key string) map[string]string {
+ dicts, _ := c.GetQueryMap(key)
+ return dicts
+}
+
+// GetQueryMap returns a map for a given query key, plus a boolean value
+// whether at least one value exists for the given key.
+func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
+ return c.get(c.Request.URL.Query(), key)
+}
+
// PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) string {
@@ -402,8 +430,11 @@ func (c *Context) PostFormArray(key string) []string {
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request
- req.ParseForm()
- req.ParseMultipartForm(c.engine.MaxMultipartMemory)
+ if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
+ if err != http.ErrNotMultipart {
+ debugPrint("error on parse multipart form array: %v", err)
+ }
+ }
if values := req.PostForm[key]; len(values) > 0 {
return values, true
}
@@ -415,8 +446,52 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false
}
+// PostFormMap returns a map for a given form key.
+func (c *Context) PostFormMap(key string) map[string]string {
+ dicts, _ := c.GetPostFormMap(key)
+ return dicts
+}
+
+// GetPostFormMap returns a map for a given form key, plus a boolean value
+// whether at least one value exists for the given key.
+func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
+ req := c.Request
+ if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
+ if err != http.ErrNotMultipart {
+ debugPrint("error on parse multipart form map: %v", err)
+ }
+ }
+ dicts, exist := c.get(req.PostForm, key)
+
+ if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
+ dicts, exist = c.get(req.MultipartForm.Value, key)
+ }
+
+ return dicts, exist
+}
+
+// get is an internal method and returns a map which satisfy conditions.
+func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
+ dicts := make(map[string]string)
+ exist := false
+ for k, v := range m {
+ if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
+ if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
+ exist = true
+ dicts[k[i+1:][:j]] = v[0]
+ }
+ }
+ }
+ return dicts, exist
+}
+
// FormFile returns the first file for the provided form key.
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)
return fh, err
}
@@ -441,8 +516,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
}
defer out.Close()
- io.Copy(out, src)
- return nil
+ _, err = io.Copy(out, src)
+ return err
}
// Bind checks the Content-Type to select a binding engine automatically,
@@ -463,20 +538,40 @@ func (c *Context) BindJSON(obj interface{}) error {
return c.MustBindWith(obj, binding.JSON)
}
+// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
+func (c *Context) BindXML(obj interface{}) error {
+ return c.MustBindWith(obj, binding.XML)
+}
+
// BindQuery is a shortcut for c.MustBindWith(obj, binding.Query).
func (c *Context) BindQuery(obj interface{}) error {
return c.MustBindWith(obj, binding.Query)
}
-// MustBindWith binds the passed struct pointer using the specified binding engine.
-// It will abort the request with HTTP 400 if any error ocurrs.
-// See the binding package.
-func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
- if err = c.ShouldBindWith(obj, b); err != nil {
- c.AbortWithError(400, err).SetType(ErrorTypeBind)
- }
+// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
+func (c *Context) BindYAML(obj interface{}) error {
+ return c.MustBindWith(obj, binding.YAML)
+}
- 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,
@@ -497,31 +592,68 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
return c.ShouldBindWith(obj, binding.JSON)
}
+// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
+func (c *Context) ShouldBindXML(obj interface{}) error {
+ return c.ShouldBindWith(obj, binding.XML)
+}
+
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error {
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.
// See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
+// ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request
+// body into the context, and reuse when it is called again.
+//
+// NOTE: This method reads the body before binding. So you should use
+// ShouldBindWith for better performance if you need to call only once.
+func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
+ var body []byte
+ if cb, ok := c.Get(BodyBytesKey); ok {
+ if cbb, ok := cb.([]byte); ok {
+ body = cbb
+ }
+ }
+ if body == nil {
+ body, err = ioutil.ReadAll(c.Request.Body)
+ if err != nil {
+ return err
+ }
+ c.Set(BodyBytesKey, body)
+ }
+ return bb.BindBody(body, obj)
+}
+
// ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP {
clientIP := c.requestHeader("X-Forwarded-For")
- if index := strings.IndexByte(clientIP, ','); index >= 0 {
- clientIP = clientIP[0:index]
+ clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
+ if clientIP == "" {
+ clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
}
- clientIP = strings.TrimSpace(clientIP)
- if clientIP != "" {
- return clientIP
- }
- clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
if clientIP != "" {
return clientIP
}
@@ -568,9 +700,9 @@ func bodyAllowedForStatus(status int) bool {
switch {
case status >= 100 && status <= 199:
return false
- case status == 204:
+ case status == http.StatusNoContent:
return false
- case status == 304:
+ case status == http.StatusNotModified:
return false
}
return true
@@ -587,9 +719,9 @@ func (c *Context) Status(code int) {
func (c *Context) Header(key, value string) {
if value == "" {
c.Writer.Header().Del(key)
- } else {
- c.Writer.Header().Set(key, value)
+ return
}
+ c.Writer.Header().Set(key, value)
}
// GetHeader returns value from request headers.
@@ -633,6 +765,7 @@ func (c *Context) Cookie(name string) (string, error) {
return val, nil
}
+// Render writes the response headers and calls render.Render to render data.
func (c *Context) Render(code int, r render.Render) {
c.Status(code)
@@ -670,12 +803,30 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj})
}
+// JSONP serializes the given struct as JSON into the response body.
+// It add padding to response body to request data from a server residing in a different domain than the client.
+// It also sets the Content-Type as "application/javascript".
+func (c *Context) JSONP(code int, obj interface{}) {
+ callback := c.DefaultQuery("callback", "")
+ if callback == "" {
+ c.Render(code, render.JSON{Data: obj})
+ return
+ }
+ c.Render(code, render.JsonpJSON{Callback: callback, Data: obj})
+}
+
// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj})
}
+// AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string.
+// It also sets the Content-Type as "application/json".
+func (c *Context) AsciiJSON(code int, obj interface{}) {
+ c.Render(code, render.AsciiJSON{Data: obj})
+}
+
// XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
@@ -687,6 +838,11 @@ func (c *Context) YAML(code int, obj interface{}) {
c.Render(code, render.YAML{Data: obj})
}
+// ProtoBuf serializes the given struct as ProtoBuf into the response body.
+func (c *Context) ProtoBuf(code int, obj interface{}) {
+ c.Render(code, render.ProtoBuf{Data: obj})
+}
+
// String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) {
c.Render(code, render.String{Format: format, Data: values})
@@ -709,6 +865,16 @@ func (c *Context) Data(code int, contentType string, data []byte) {
})
}
+// DataFromReader writes the specified reader into the body stream and updates the HTTP code.
+func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) {
+ c.Render(code, render.Reader{
+ Headers: extraHeaders,
+ ContentType: contentType,
+ ContentLength: contentLength,
+ Reader: reader,
+ })
+}
+
// File writes the specified file into the body stream in a efficient way.
func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
@@ -722,6 +888,7 @@ func (c *Context) SSEvent(name string, message interface{}) {
})
}
+// Stream sends a streaming response.
func (c *Context) Stream(step func(w io.Writer) bool) {
w := c.Writer
clientGone := w.CloseNotify()
@@ -743,6 +910,7 @@ func (c *Context) Stream(step func(w io.Writer) bool) {
/******** CONTENT NEGOTIATION *******/
/************************************/
+// Negotiate contains all negotiations data.
type Negotiate struct {
Offered []string
HTMLName string
@@ -752,6 +920,7 @@ type Negotiate struct {
Data interface{}
}
+// Negotiate calls different Render according acceptable Accept format.
func (c *Context) Negotiate(code int, config Negotiate) {
switch c.NegotiateFormat(config.Offered...) {
case binding.MIMEJSON:
@@ -767,10 +936,11 @@ func (c *Context) Negotiate(code int, config Negotiate) {
c.XML(code, data)
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 {
assert1(len(offered) > 0, "you must provide at least one offer")
@@ -790,6 +960,7 @@ func (c *Context) NegotiateFormat(offered ...string) string {
return ""
}
+// SetAccepted sets Accept header data.
func (c *Context) SetAccepted(formats ...string) {
c.Accepted = formats
}
@@ -798,18 +969,33 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/
+// Deadline returns the time when work done on behalf of this context
+// should be canceled. Deadline returns ok==false when no deadline is
+// set. Successive calls to Deadline return the same results.
func (c *Context) Deadline() (deadline time.Time, ok bool) {
return
}
+// Done returns a channel that's closed when work done on behalf of this
+// context should be canceled. Done may return nil if this context can
+// never be canceled. Successive calls to Done return the same value.
func (c *Context) Done() <-chan struct{} {
return nil
}
+// Err returns a non-nil error value after Done is closed,
+// successive calls to Err return the same error.
+// If Done is not yet closed, Err returns nil.
+// If Done is closed, Err returns a non-nil error explaining why:
+// Canceled if the context was canceled
+// or DeadlineExceeded if the context's deadline passed.
func (c *Context) Err() error {
return nil
}
+// Value returns the value associated with this context for key, or nil
+// if no value is associated with key. Successive calls to Value with
+// the same key returns the same result.
func (c *Context) Value(key interface{}) interface{} {
if key == 0 {
return c.Request
diff --git a/context_17.go b/context_17.go
new file mode 100644
index 00000000..8e9f75ad
--- /dev/null
+++ b/context_17.go
@@ -0,0 +1,17 @@
+// Copyright 2018 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+// +build go1.7
+
+package gin
+
+import (
+ "github.com/gin-gonic/gin/render"
+)
+
+// PureJSON serializes the given struct as JSON into the response body.
+// PureJSON, unlike JSON, does not replace special html characters with their unicode entities.
+func (c *Context) PureJSON(code int, obj interface{}) {
+ c.Render(code, render.PureJSON{Data: obj})
+}
diff --git a/context_17_test.go b/context_17_test.go
new file mode 100644
index 00000000..5b9ebcdc
--- /dev/null
+++ b/context_17_test.go
@@ -0,0 +1,27 @@
+// Copyright 2018 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+// +build go1.7
+
+package gin
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// Tests that the response is serialized as JSON
+// and Content-Type is set to application/json
+// and special HTML characters are preserved
+func TestContextRenderPureJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": ""})
+ assert.Equal(t, http.StatusCreated, w.Code)
+ assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String())
+ assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
+}
diff --git a/context_test.go b/context_test.go
index 9024cfc1..dc8ac306 100644
--- a/context_test.go
+++ b/context_test.go
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"html/template"
+ "io"
"mime/multipart"
"net/http"
"net/http/httptest"
@@ -18,8 +19,12 @@ import (
"time"
"github.com/gin-contrib/sse"
+ "github.com/gin-gonic/gin/binding"
+ "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
+
+ testdata "github.com/gin-gonic/gin/testdata/protoexample"
)
var _ context.Context = &Context{}
@@ -46,6 +51,8 @@ func createMultipartRequest() *http.Request {
must(mw.WriteField("time_local", "31/12/2016 14:55"))
must(mw.WriteField("time_utc", "31/12/2016 14:55"))
must(mw.WriteField("time_location", "31/12/2016 14:55"))
+ must(mw.WriteField("names[a]", "thinkerou"))
+ must(mw.WriteField("names[b]", "tianou"))
req, err := http.NewRequest("POST", "/", body)
must(err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
@@ -63,7 +70,8 @@ func TestContextFormFile(t *testing.T) {
mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) {
- w.Write([]byte("test"))
+ _, err = w.Write([]byte("test"))
+ assert.NoError(t, err)
}
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
@@ -77,13 +85,27 @@ func TestContextFormFile(t *testing.T) {
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) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
- mw.WriteField("foo", "bar")
+ assert.NoError(t, mw.WriteField("foo", "bar"))
w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) {
- w.Write([]byte("test"))
+ _, err = w.Write([]byte("test"))
+ assert.NoError(t, err)
}
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
@@ -117,7 +139,8 @@ func TestSaveUploadedCreateFailed(t *testing.T) {
mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) {
- w.Write([]byte("test"))
+ _, err = w.Write([]byte("test"))
+ assert.NoError(t, err)
}
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
@@ -139,7 +162,7 @@ func TestContextReset(t *testing.T) {
c.index = 2
c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()}
c.Params = Params{Param{}}
- c.Error(errors.New("test"))
+ c.Error(errors.New("test")) // nolint: errcheck
c.Set("foo", "bar")
c.reset()
@@ -308,6 +331,8 @@ func TestContextCopy(t *testing.T) {
assert.Equal(t, cp.Keys, c.Keys)
assert.Equal(t, cp.engine, c.engine)
assert.Equal(t, cp.Params, c.Params)
+ cp.Set("foo", "notBar")
+ assert.False(t, cp.Keys["foo"] == c.Keys["foo"])
}
func TestContextHandlerName(t *testing.T) {
@@ -317,10 +342,26 @@ func TestContextHandlerName(t *testing.T) {
assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName())
}
+func TestContextHandlerNames(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest, func(c *Context) {}, handlerNameTest2}
+
+ names := c.HandlerNames()
+
+ assert.True(t, len(names) == 4)
+ for _, name := range names {
+ assert.Regexp(t, `^(.*/vendor/)?(github\.com/gin-gonic/gin\.){1}(TestContextHandlerNames\.func.*){0,1}(handlerNameTest.*){0,1}`, name)
+ }
+}
+
func handlerNameTest(c *Context) {
}
+func handlerNameTest2(c *Context) {
+
+}
+
var handlerTest HandlerFunc = func(c *Context) {
}
@@ -370,7 +411,8 @@ func TestContextQuery(t *testing.T) {
func TestContextQueryAndPostForm(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
- c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body)
+ c.Request, _ = http.NewRequest("POST",
+ "/?both=GET&id=main&id=omit&array[]=first&array[]=second&ids[a]=hi&ids[b]=3.14", body)
c.Request.Header.Add("Content-Type", MIMEPOSTForm)
assert.Equal(t, "bar", c.DefaultPostForm("foo", "none"))
@@ -438,6 +480,30 @@ func TestContextQueryAndPostForm(t *testing.T) {
values = c.QueryArray("both")
assert.Equal(t, 1, len(values))
assert.Equal(t, "GET", values[0])
+
+ dicts, ok := c.GetQueryMap("ids")
+ assert.True(t, ok)
+ assert.Equal(t, "hi", dicts["a"])
+ assert.Equal(t, "3.14", dicts["b"])
+
+ dicts, ok = c.GetQueryMap("nokey")
+ assert.False(t, ok)
+ assert.Equal(t, 0, len(dicts))
+
+ dicts, ok = c.GetQueryMap("both")
+ assert.False(t, ok)
+ assert.Equal(t, 0, len(dicts))
+
+ dicts, ok = c.GetQueryMap("array")
+ assert.False(t, ok)
+ assert.Equal(t, 0, len(dicts))
+
+ dicts = c.QueryMap("ids")
+ assert.Equal(t, "hi", dicts["a"])
+ assert.Equal(t, "3.14", dicts["b"])
+
+ dicts = c.QueryMap("nokey")
+ assert.Equal(t, 0, len(dicts))
}
func TestContextPostFormMultipart(t *testing.T) {
@@ -514,6 +580,22 @@ func TestContextPostFormMultipart(t *testing.T) {
values = c.PostFormArray("foo")
assert.Equal(t, 1, len(values))
assert.Equal(t, "bar", values[0])
+
+ dicts, ok := c.GetPostFormMap("names")
+ assert.True(t, ok)
+ assert.Equal(t, "thinkerou", dicts["a"])
+ assert.Equal(t, "tianou", dicts["b"])
+
+ dicts, ok = c.GetPostFormMap("nokey")
+ assert.False(t, ok)
+ assert.Equal(t, 0, len(dicts))
+
+ dicts = c.PostFormMap("names")
+ assert.Equal(t, "thinkerou", dicts["a"])
+ assert.Equal(t, "tianou", dicts["b"])
+
+ dicts = c.PostFormMap("nokey")
+ assert.Equal(t, 0, len(dicts))
}
func TestContextSetCookie(t *testing.T) {
@@ -540,10 +622,11 @@ func TestContextGetCookie(t *testing.T) {
}
func TestContextBodyAllowedForStatus(t *testing.T) {
+ // todo(thinkerou): go1.6 not support StatusProcessing
assert.False(t, false, bodyAllowedForStatus(102))
- assert.False(t, false, bodyAllowedForStatus(204))
- assert.False(t, false, bodyAllowedForStatus(304))
- assert.True(t, true, bodyAllowedForStatus(500))
+ assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent))
+ assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified))
+ assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError))
}
type TestPanicRender struct {
@@ -570,15 +653,44 @@ func TestContextRenderPanicIfErr(t *testing.T) {
// Tests that the response is serialized as JSON
// and Content-Type is set to application/json
+// and special HTML characters are escaped
func TestContextRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.JSON(201, H{"foo": "bar"})
+ c.JSON(http.StatusCreated, H{"foo": "bar", "html": ""})
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
+ assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
+ assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
+}
+
+// Tests that the response is serialized as JSONP
+// and Content-Type is set to application/javascript
+func TestContextRenderJSONP(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.Request, _ = http.NewRequest("GET", "http://example.com/?callback=x", nil)
+
+ c.JSONP(http.StatusCreated, H{"foo": "bar"})
+
+ assert.Equal(t, http.StatusCreated, w.Code)
+ assert.Equal(t, "x({\"foo\":\"bar\"})", w.Body.String())
+ assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
+}
+
+// Tests that the response is serialized as JSONP
+// and Content-Type is set to application/json
+func TestContextRenderJSONPWithoutCallback(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.Request, _ = http.NewRequest("GET", "http://example.com", nil)
+
+ c.JSONP(http.StatusCreated, H{"foo": "bar"})
+
+ assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
- assert.Equal(t, "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
@@ -586,11 +698,11 @@ func TestContextRenderNoContentJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.JSON(204, H{"foo": "bar"})
+ c.JSON(http.StatusNoContent, H{"foo": "bar"})
- assert.Equal(t, 204, w.Code)
+ assert.Equal(t, http.StatusNoContent, w.Code)
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
@@ -600,11 +712,11 @@ func TestContextRenderAPIJSON(t *testing.T) {
c, _ := CreateTestContext(w)
c.Header("Content-Type", "application/vnd.api+json")
- c.JSON(201, H{"foo": "bar"})
+ c.JSON(http.StatusCreated, H{"foo": "bar"})
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
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
@@ -613,11 +725,11 @@ func TestContextRenderNoContentAPIJSON(t *testing.T) {
c, _ := CreateTestContext(w)
c.Header("Content-Type", "application/vnd.api+json")
- c.JSON(204, H{"foo": "bar"})
+ c.JSON(http.StatusNoContent, H{"foo": "bar"})
- assert.Equal(t, 204, w.Code)
+ assert.Equal(t, http.StatusNoContent, w.Code)
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
@@ -626,11 +738,11 @@ func TestContextRenderIndentedJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
+ c.IndentedJSON(http.StatusCreated, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
- assert.Equal(t, 201, 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, "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
@@ -638,11 +750,11 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
+ c.IndentedJSON(http.StatusNoContent, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
- assert.Equal(t, 204, w.Code)
+ assert.Equal(t, http.StatusNoContent, w.Code)
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
@@ -652,11 +764,11 @@ func TestContextRenderSecureJSON(t *testing.T) {
c, router := CreateTestContext(w)
router.SecureJsonPrefix("&&&START&&&")
- c.SecureJSON(201, []string{"foo", "bar"})
+ c.SecureJSON(http.StatusCreated, []string{"foo", "bar"})
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
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
@@ -664,11 +776,22 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.SecureJSON(204, []string{"foo", "bar"})
+ c.SecureJSON(http.StatusNoContent, []string{"foo", "bar"})
- assert.Equal(t, 204, w.Code)
+ assert.Equal(t, http.StatusNoContent, w.Code)
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) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.AsciiJSON(http.StatusNoContent, []string{"lang", "Go语言"})
+
+ assert.Equal(t, http.StatusNoContent, w.Code)
+ assert.Empty(t, w.Body.String())
+ assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
}
// Tests that the response executes the templates
@@ -680,11 +803,11 @@ func TestContextRenderHTML(t *testing.T) {
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
- c.HTML(201, "t", H{"name": "alexandernyquist"})
+ c.HTML(http.StatusCreated, "t", H{"name": "alexandernyquist"})
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
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) {
@@ -695,20 +818,20 @@ func TestContextRenderHTML2(t *testing.T) {
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 1)
- var b bytes.Buffer
- setup(&b)
- defer teardown()
-
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(201, "t", H{"name": "alexandernyquist"})
+ c.HTML(http.StatusCreated, "t", H{"name": "alexandernyquist"})
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
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
@@ -718,11 +841,11 @@ func TestContextRenderNoContentHTML(t *testing.T) {
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
- c.HTML(204, "t", H{"name": "alexandernyquist"})
+ c.HTML(http.StatusNoContent, "t", H{"name": "alexandernyquist"})
- assert.Equal(t, 204, w.Code)
+ assert.Equal(t, http.StatusNoContent, w.Code)
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
@@ -731,11 +854,11 @@ func TestContextRenderXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.XML(201, H{"foo": "bar"})
+ c.XML(http.StatusCreated, H{"foo": "bar"})
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(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"))
}
// Tests that no XML is rendered if code is 204
@@ -743,11 +866,11 @@ func TestContextRenderNoContentXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.XML(204, H{"foo": "bar"})
+ c.XML(http.StatusNoContent, H{"foo": "bar"})
- assert.Equal(t, 204, w.Code)
+ assert.Equal(t, http.StatusNoContent, w.Code)
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
@@ -756,11 +879,11 @@ func TestContextRenderString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.String(201, "test %s %d", "string", 2)
+ c.String(http.StatusCreated, "test %s %d", "string", 2)
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
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
@@ -768,11 +891,11 @@ func TestContextRenderNoContentString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.String(204, "test %s %d", "string", 2)
+ c.String(http.StatusNoContent, "test %s %d", "string", 2)
- assert.Equal(t, 204, w.Code)
+ assert.Equal(t, http.StatusNoContent, w.Code)
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
@@ -782,11 +905,11 @@ func TestContextRenderHTMLString(t *testing.T) {
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8")
- c.String(201, "%s %d", "string", 3)
+ c.String(http.StatusCreated, "%s %d", "string", 3)
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "string 3", 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
@@ -795,11 +918,11 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8")
- c.String(204, "%s %d", "string", 3)
+ c.String(http.StatusNoContent, "%s %d", "string", 3)
- assert.Equal(t, 204, w.Code)
+ assert.Equal(t, http.StatusNoContent, w.Code)
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`
@@ -808,11 +931,11 @@ func TestContextRenderData(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.Data(201, "text/csv", []byte(`foo,bar`))
+ c.Data(http.StatusCreated, "text/csv", []byte(`foo,bar`))
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
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
@@ -820,11 +943,11 @@ func TestContextRenderNoContentData(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.Data(204, "text/csv", []byte(`foo,bar`))
+ c.Data(http.StatusNoContent, "text/csv", []byte(`foo,bar`))
- assert.Equal(t, 204, w.Code)
+ assert.Equal(t, http.StatusNoContent, w.Code)
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) {
@@ -851,9 +974,9 @@ func TestContextRenderFile(t *testing.T) {
c.Request, _ = http.NewRequest("GET", "/", nil)
c.File("./gin.go")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
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
@@ -862,11 +985,35 @@ func TestContextRenderYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.YAML(201, H{"foo": "bar"})
+ c.YAML(http.StatusCreated, H{"foo": "bar"})
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
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
+// and Content-Type is set to application/x-protobuf
+// and we just use the example protobuf to check if the response is correct
+func TestContextRenderProtoBuf(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ reps := []int64{int64(1), int64(2)}
+ label := "test"
+ data := &testdata.Test{
+ Label: &label,
+ Reps: reps,
+ }
+
+ c.ProtoBuf(http.StatusCreated, data)
+
+ protoData, err := proto.Marshal(data)
+ assert.NoError(t, err)
+
+ assert.Equal(t, http.StatusCreated, w.Code)
+ assert.Equal(t, string(protoData), w.Body.String())
+ assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
}
func TestContextHeaders(t *testing.T) {
@@ -894,9 +1041,9 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) {
assert.Panics(t, func() { c.Redirect(299, "/new_path") })
assert.Panics(t, func() { c.Redirect(309, "/new_path") })
- c.Redirect(301, "/path")
+ c.Redirect(http.StatusMovedPermanently, "/path")
c.Writer.WriteHeaderNow()
- assert.Equal(t, 301, w.Code)
+ assert.Equal(t, http.StatusMovedPermanently, w.Code)
assert.Equal(t, "/path", w.Header().Get("Location"))
}
@@ -905,10 +1052,10 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
- c.Redirect(302, "http://google.com")
+ c.Redirect(http.StatusFound, "http://google.com")
c.Writer.WriteHeaderNow()
- assert.Equal(t, 302, w.Code)
+ assert.Equal(t, http.StatusFound, w.Code)
assert.Equal(t, "http://google.com", w.Header().Get("Location"))
}
@@ -917,21 +1064,23 @@ func TestContextRenderRedirectWith201(t *testing.T) {
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
- c.Redirect(201, "/resource")
+ c.Redirect(http.StatusCreated, "/resource")
c.Writer.WriteHeaderNow()
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "/resource", w.Header().Get("Location"))
}
func TestContextRenderRedirectAll(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
- assert.Panics(t, func() { c.Redirect(200, "/resource") })
- assert.Panics(t, func() { c.Redirect(202, "/resource") })
+ assert.Panics(t, func() { c.Redirect(http.StatusOK, "/resource") })
+ assert.Panics(t, func() { c.Redirect(http.StatusAccepted, "/resource") })
assert.Panics(t, func() { c.Redirect(299, "/resource") })
assert.Panics(t, func() { c.Redirect(309, "/resource") })
- assert.NotPanics(t, func() { c.Redirect(300, "/resource") })
+ assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") })
+ // todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
+ // when we upgrade go version we can use http.StatusPermanentRedirect
assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
}
@@ -940,14 +1089,14 @@ func TestContextNegotiationWithJSON(t *testing.T) {
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
- c.Negotiate(200, Negotiate{
+ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEJSON, MIMEXML},
Data: H{"foo": "bar"},
})
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
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) {
@@ -955,14 +1104,14 @@ func TestContextNegotiationWithXML(t *testing.T) {
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
- c.Negotiate(200, Negotiate{
+ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEXML, MIMEJSON},
Data: H{"foo": "bar"},
})
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(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"))
}
func TestContextNegotiationWithHTML(t *testing.T) {
@@ -972,15 +1121,15 @@ func TestContextNegotiationWithHTML(t *testing.T) {
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
- c.Negotiate(200, Negotiate{
+ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEHTML},
Data: H{"name": "gin"},
HTMLName: "t",
})
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
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) {
@@ -988,11 +1137,11 @@ func TestContextNegotiationNotSupport(t *testing.T) {
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
- c.Negotiate(200, Negotiate{
+ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEPOSTForm},
})
- assert.Equal(t, 406, w.Code)
+ assert.Equal(t, http.StatusNotAcceptable, w.Code)
assert.Equal(t, c.index, abortIndex)
assert.True(t, c.IsAborted())
}
@@ -1016,7 +1165,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) {
assert.Empty(t, c.NegotiateFormat(MIMEJSON))
}
-func TestContextNegotiationFormatCustum(t *testing.T) {
+func TestContextNegotiationFormatCustom(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
@@ -1050,11 +1199,11 @@ func TestContextAbortWithStatus(t *testing.T) {
c, _ := CreateTestContext(w)
c.index = 4
- c.AbortWithStatus(401)
+ c.AbortWithStatus(http.StatusUnauthorized)
assert.Equal(t, abortIndex, c.index)
- assert.Equal(t, 401, c.Writer.Status())
- assert.Equal(t, 401, w.Code)
+ assert.Equal(t, http.StatusUnauthorized, c.Writer.Status())
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.True(t, c.IsAborted())
}
@@ -1072,18 +1221,19 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
in.Bar = "barValue"
in.Foo = "fooValue"
- c.AbortWithStatusJSON(415, in)
+ c.AbortWithStatusJSON(http.StatusUnsupportedMediaType, in)
assert.Equal(t, abortIndex, c.index)
- assert.Equal(t, 415, c.Writer.Status())
- assert.Equal(t, 415, w.Code)
+ assert.Equal(t, http.StatusUnsupportedMediaType, c.Writer.Status())
+ assert.Equal(t, http.StatusUnsupportedMediaType, w.Code)
assert.True(t, c.IsAborted())
contentType := w.Header().Get("Content-Type")
assert.Equal(t, "application/json; charset=utf-8", contentType)
buf := new(bytes.Buffer)
- buf.ReadFrom(w.Body)
+ _, err := buf.ReadFrom(w.Body)
+ assert.NoError(t, err)
jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
}
@@ -1092,11 +1242,11 @@ func TestContextError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
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.Equal(t, "Error #01: first error\n", c.Errors.String())
- c.Error(&Error{
+ c.Error(&Error{ // nolint: errcheck
Err: errors.New("second error"),
Meta: "some data 2",
Type: ErrorTypePublic,
@@ -1118,13 +1268,13 @@ func TestContextError(t *testing.T) {
t.Error("didn't panic")
}
}()
- c.Error(nil)
+ c.Error(nil) // nolint: errcheck
}
func TestContextTypedError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
- c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
- c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
+ c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) // nolint: errcheck
+ c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) // nolint: errcheck
for _, err := range c.Errors.ByType(ErrorTypePublic) {
assert.Equal(t, ErrorTypePublic, err.Type)
@@ -1139,9 +1289,9 @@ func TestContextAbortWithError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
+ c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") // nolint: errcheck
- assert.Equal(t, 401, w.Code)
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, abortIndex, c.index)
assert.True(t, c.IsAborted())
}
@@ -1215,6 +1365,26 @@ func TestContextBindWithJSON(t *testing.T) {
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
+func TestContextBindWithXML(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`
+
+ FOO
+ BAR
+ `))
+ c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
+
+ var obj struct {
+ Foo string `xml:"foo"`
+ Bar string `xml:"bar"`
+ }
+ assert.NoError(t, c.BindXML(&obj))
+ assert.Equal(t, "FOO", obj.Foo)
+ assert.Equal(t, "BAR", obj.Bar)
+ assert.Equal(t, 0, w.Body.Len())
+}
func TestContextBindWithQuery(t *testing.T) {
w := httptest.NewRecorder()
@@ -1232,6 +1402,23 @@ func TestContextBindWithQuery(t *testing.T) {
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) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@@ -1249,7 +1436,7 @@ func TestContextBadAutoBind(t *testing.T) {
assert.Empty(t, obj.Bar)
assert.Empty(t, obj.Foo)
- assert.Equal(t, 400, w.Code)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
assert.True(t, c.IsAborted())
}
@@ -1285,19 +1472,61 @@ func TestContextShouldBindWithJSON(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
+func TestContextShouldBindWithXML(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`
+
+ FOO
+ BAR
+ `))
+ c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
+
+ var obj struct {
+ Foo string `xml:"foo"`
+ Bar string `xml:"bar"`
+ }
+ assert.NoError(t, c.ShouldBindXML(&obj))
+ assert.Equal(t, "FOO", obj.Foo)
+ assert.Equal(t, "BAR", obj.Bar)
+ assert.Equal(t, 0, w.Body.Len())
+}
+
func TestContextShouldBindWithQuery(t *testing.T) {
w := httptest.NewRecorder()
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 {
- Foo string `form:"foo"`
- Bar string `form:"bar"`
+ Foo string `form:"foo"`
+ Bar string `form:"bar"`
+ Foo1 string `form:"Foo"`
+ Bar1 string `form:"Bar"`
}
assert.NoError(t, c.ShouldBindQuery(&obj))
assert.Equal(t, "foo", obj.Bar)
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())
}
@@ -1320,6 +1549,85 @@ func TestContextBadAutoShouldBind(t *testing.T) {
assert.False(t, c.IsAborted())
}
+func TestContextShouldBindBodyWith(t *testing.T) {
+ type typeA struct {
+ Foo string `json:"foo" xml:"foo" binding:"required"`
+ }
+ type typeB struct {
+ Bar string `json:"bar" xml:"bar" binding:"required"`
+ }
+ for _, tt := range []struct {
+ name string
+ bindingA, bindingB binding.BindingBody
+ bodyA, bodyB string
+ }{
+ {
+ name: "JSON & JSON",
+ bindingA: binding.JSON,
+ bindingB: binding.JSON,
+ bodyA: `{"foo":"FOO"}`,
+ bodyB: `{"bar":"BAR"}`,
+ },
+ {
+ name: "JSON & XML",
+ bindingA: binding.JSON,
+ bindingB: binding.XML,
+ bodyA: `{"foo":"FOO"}`,
+ bodyB: `
+
+ BAR
+`,
+ },
+ {
+ name: "XML & XML",
+ bindingA: binding.XML,
+ bindingB: binding.XML,
+ bodyA: `
+
+ FOO
+`,
+ bodyB: `
+
+ BAR
+`,
+ },
+ } {
+ t.Logf("testing: %s", tt.name)
+ // bodyA to typeA and typeB
+ {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.Request, _ = http.NewRequest(
+ "POST", "http://example.com", bytes.NewBufferString(tt.bodyA),
+ )
+ // When it binds to typeA and typeB, it finds the body is
+ // not typeB but typeA.
+ objA := typeA{}
+ assert.NoError(t, c.ShouldBindBodyWith(&objA, tt.bindingA))
+ assert.Equal(t, typeA{"FOO"}, objA)
+ objB := typeB{}
+ assert.Error(t, c.ShouldBindBodyWith(&objB, tt.bindingB))
+ assert.NotEqual(t, typeB{"BAR"}, objB)
+ }
+ // bodyB to typeA and typeB
+ {
+ // When it binds to typeA and typeB, it finds the body is
+ // not typeA but typeB.
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.Request, _ = http.NewRequest(
+ "POST", "http://example.com", bytes.NewBufferString(tt.bodyB),
+ )
+ objA := typeA{}
+ assert.Error(t, c.ShouldBindBodyWith(&objA, tt.bindingA))
+ assert.NotEqual(t, typeA{"FOO"}, objA)
+ objB := typeB{}
+ assert.NoError(t, c.ShouldBindBodyWith(&objB, tt.bindingB))
+ assert.Equal(t, typeB{"BAR"}, objB)
+ }
+ }
+}
+
func TestContextGolangContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
@@ -1377,3 +1685,91 @@ func TestContextGetRawData(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, "Fetch binary post data", string(data))
}
+
+func TestContextRenderDataFromReader(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ body := "#!PNG some raw data"
+ reader := strings.NewReader(body)
+ contentLength := int64(len(body))
+ contentType := "image/png"
+ extraHeaders := map[string]string{"Content-Disposition": `attachment; filename="gopher.png"`}
+
+ c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(t, body, w.Body.String())
+ assert.Equal(t, contentType, w.Header().Get("Content-Type"))
+ assert.Equal(t, fmt.Sprintf("%d", contentLength), w.Header().Get("Content-Length"))
+ assert.Equal(t, extraHeaders["Content-Disposition"], w.Header().Get("Content-Disposition"))
+}
+
+type TestResponseRecorder struct {
+ *httptest.ResponseRecorder
+ closeChannel chan bool
+}
+
+func (r *TestResponseRecorder) CloseNotify() <-chan bool {
+ return r.closeChannel
+}
+
+func (r *TestResponseRecorder) closeClient() {
+ r.closeChannel <- true
+}
+
+func CreateTestResponseRecorder() *TestResponseRecorder {
+ return &TestResponseRecorder{
+ httptest.NewRecorder(),
+ make(chan bool, 1),
+ }
+}
+
+func TestContextStream(t *testing.T) {
+ w := CreateTestResponseRecorder()
+ c, _ := CreateTestContext(w)
+
+ stopStream := true
+ c.Stream(func(w io.Writer) bool {
+ defer func() {
+ stopStream = false
+ }()
+
+ _, err := w.Write([]byte("test"))
+ assert.NoError(t, err)
+
+ return stopStream
+ })
+
+ assert.Equal(t, "testtest", w.Body.String())
+}
+
+func TestContextStreamWithClientGone(t *testing.T) {
+ w := CreateTestResponseRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.Stream(func(writer io.Writer) bool {
+ defer func() {
+ w.closeClient()
+ }()
+
+ _, err := writer.Write([]byte("test"))
+ assert.NoError(t, err)
+
+ return true
+ })
+
+ 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()
+ })
+}
diff --git a/coverage.sh b/coverage.sh
deleted file mode 100644
index 81437f91..00000000
--- a/coverage.sh
+++ /dev/null
@@ -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$'); 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
diff --git a/debug.go b/debug.go
index 897c4943..98c67cf7 100644
--- a/debug.go
+++ b/debug.go
@@ -6,13 +6,15 @@ package gin
import (
"bytes"
+ "fmt"
"html/template"
- "log"
+ "os"
+ "runtime"
+ "strconv"
+ "strings"
)
-func init() {
- log.SetFlags(0)
-}
+const ginSupportMinGoVer = 6
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@@ -20,11 +22,18 @@ func IsDebugging() bool {
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) {
if IsDebugging() {
nuHandlers := len(handlers)
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,11 +51,28 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
func debugPrint(format string, values ...interface{}) {
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() {
+ 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.
`)
diff --git a/debug_test.go b/debug_test.go
index dfd54c82..d338f0a0 100644
--- a/debug_test.go
+++ b/debug_test.go
@@ -11,6 +11,8 @@ import (
"io"
"log"
"os"
+ "runtime"
+ "sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -30,86 +32,122 @@ func TestIsDebugging(t *testing.T) {
}
func TestDebugPrint(t *testing.T) {
- var w bytes.Buffer
- setup(&w)
- defer teardown()
-
- SetMode(ReleaseMode)
- debugPrint("DEBUG this!")
- SetMode(TestMode)
- debugPrint("DEBUG this!")
- assert.Empty(t, w.String())
-
- SetMode(DebugMode)
- debugPrint("these are %d %s\n", 2, "error messages")
- assert.Equal(t, "[GIN-debug] these are 2 error messages\n", w.String())
+ re := captureOutput(t, func() {
+ SetMode(DebugMode)
+ SetMode(ReleaseMode)
+ debugPrint("DEBUG this!")
+ SetMode(TestMode)
+ debugPrint("DEBUG this!")
+ SetMode(DebugMode)
+ debugPrint("these are %d %s", 2, "error messages")
+ SetMode(TestMode)
+ })
+ assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
}
func TestDebugPrintError(t *testing.T) {
- var w bytes.Buffer
- setup(&w)
- defer teardown()
-
- SetMode(DebugMode)
- debugPrintError(nil)
- assert.Empty(t, w.String())
-
- debugPrintError(errors.New("this is an error"))
- assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", w.String())
+ re := captureOutput(t, func() {
+ SetMode(DebugMode)
+ debugPrintError(nil)
+ debugPrintError(errors.New("this is an error"))
+ SetMode(TestMode)
+ })
+ assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", re)
}
func TestDebugPrintRoutes(t *testing.T) {
- var w bytes.Buffer
- setup(&w)
- defer teardown()
-
- 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())
+ re := captureOutput(t, func() {
+ SetMode(DebugMode)
+ debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
+ SetMode(TestMode)
+ })
+ 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) {
- var w bytes.Buffer
- setup(&w)
- defer teardown()
-
- templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl"))
- debugPrintLoadTemplate(templ)
- assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, w.String())
+ re := captureOutput(t, func() {
+ SetMode(DebugMode)
+ templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
+ debugPrintLoadTemplate(templ)
+ SetMode(TestMode)
+ })
+ assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, re)
}
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
- var w bytes.Buffer
- setup(&w)
- defer teardown()
-
- 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())
+ re := captureOutput(t, func() {
+ SetMode(DebugMode)
+ debugPrintWARNINGSetHTMLTemplate()
+ 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", re)
}
func TestDebugPrintWARNINGDefault(t *testing.T) {
- var w bytes.Buffer
- setup(&w)
- defer teardown()
-
- debugPrintWARNINGDefault()
- assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", w.String())
+ re := captureOutput(t, func() {
+ SetMode(DebugMode)
+ debugPrintWARNINGDefault()
+ SetMode(TestMode)
+ })
+ 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) {
- var w bytes.Buffer
- setup(&w)
- defer teardown()
-
- 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())
+ re := captureOutput(t, func() {
+ SetMode(DebugMode)
+ debugPrintWARNINGNew()
+ SetMode(TestMode)
+ })
+ 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) {
- SetMode(DebugMode)
- log.SetOutput(w)
+func captureOutput(t *testing.T, f func()) string {
+ reader, writer, err := os.Pipe()
+ 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() {
- SetMode(TestMode)
- log.SetOutput(os.Stdout)
+func TestGetMinVer(t *testing.T) {
+ var m uint64
+ var e error
+ _, e = getMinVer("go1")
+ assert.NotNil(t, e)
+ m, e = getMinVer("go1.1")
+ assert.Equal(t, uint64(1), m)
+ assert.Nil(t, e)
+ m, e = getMinVer("go1.1.1")
+ assert.Nil(t, e)
+ assert.Equal(t, uint64(1), m)
+ _, e = getMinVer("go1.1.1.1")
+ assert.NotNil(t, e)
}
diff --git a/deprecated_test.go b/deprecated_test.go
index 7a875fe4..f8df651c 100644
--- a/deprecated_test.go
+++ b/deprecated_test.go
@@ -24,7 +24,9 @@ func TestBindWith(t *testing.T) {
Foo string `form:"foo"`
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, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
diff --git a/docs/how-to-build-an-effective-middleware.md b/docs/how-to-build-an-effective-middleware.md
new file mode 100644
index 00000000..568d5720
--- /dev/null
+++ b/docs/how-to-build-an-effective-middleware.md
@@ -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
+```
diff --git a/errors.go b/errors.go
index dbfccd85..ab13ca61 100644
--- a/errors.go
+++ b/errors.go
@@ -9,21 +9,28 @@ import (
"fmt"
"reflect"
- "github.com/gin-gonic/gin/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
const (
- ErrorTypeBind ErrorType = 1 << 63 // used when c.Bind() fails
- ErrorTypeRender ErrorType = 1 << 62 // used when c.Render() fails
+ // ErrorTypeBind is used when Context.Bind() 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
- ErrorTypePublic ErrorType = 1 << 1
-
+ // ErrorTypePublic indicates a public error.
+ ErrorTypePublic ErrorType = 1 << 1
+ // ErrorTypeAny indicates any other error.
ErrorTypeAny ErrorType = 1<<64 - 1
- ErrorTypeNu = 2
+ // ErrorTypeNu indicates any other error.
+ ErrorTypeNu = 2
)
+// Error represents a error's specification.
type Error struct {
Err error
Type ErrorType
@@ -34,16 +41,19 @@ type errorMsgs []*Error
var _ error = &Error{}
+// SetType sets the error's type.
func (msg *Error) SetType(flags ErrorType) *Error {
msg.Type = flags
return msg
}
+// SetMeta sets the error's meta data.
func (msg *Error) SetMeta(data interface{}) *Error {
msg.Meta = data
return msg
}
+// JSON creates a properly formated JSON
func (msg *Error) JSON() interface{} {
json := H{}
if msg.Meta != nil {
@@ -70,11 +80,12 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(msg.JSON())
}
-// Error implements the error interface
+// Error implements the error interface.
func (msg Error) Error() string {
return msg.Err.Error()
}
+// IsType judges one error.
func (msg *Error) IsType(flags ErrorType) bool {
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) {
return json.Marshal(a.JSON())
}
diff --git a/errors_test.go b/errors_test.go
index a666d7c1..6aae1c10 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -8,7 +8,7 @@ import (
"errors"
"testing"
- "github.com/gin-gonic/gin/json"
+ "github.com/gin-gonic/gin/internal/json"
"github.com/stretchr/testify/assert"
)
@@ -19,47 +19,47 @@ func TestError(t *testing.T) {
Type: ErrorTypePrivate,
}
assert.Equal(t, err.Error(), baseError.Error())
- assert.Equal(t, err.JSON(), H{"error": baseError.Error()})
+ assert.Equal(t, H{"error": baseError.Error()}, err.JSON())
assert.Equal(t, err.SetType(ErrorTypePublic), err)
- assert.Equal(t, err.Type, ErrorTypePublic)
+ assert.Equal(t, ErrorTypePublic, err.Type)
assert.Equal(t, err.SetMeta("some data"), err)
- assert.Equal(t, err.Meta, "some data")
- assert.Equal(t, err.JSON(), H{
+ assert.Equal(t, "some data", err.Meta)
+ assert.Equal(t, H{
"error": baseError.Error(),
"meta": "some data",
- })
+ }, err.JSON())
jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
- err.SetMeta(H{
+ err.SetMeta(H{ // nolint: errcheck
"status": "200",
"data": "some data",
})
- assert.Equal(t, err.JSON(), H{
+ assert.Equal(t, H{
"error": baseError.Error(),
"status": "200",
"data": "some data",
- })
+ }, err.JSON())
- err.SetMeta(H{
+ err.SetMeta(H{ // nolint: errcheck
"error": "custom error",
"status": "200",
"data": "some data",
})
- assert.Equal(t, err.JSON(), H{
+ assert.Equal(t, H{
"error": "custom error",
"status": "200",
"data": "some data",
- })
+ }, err.JSON())
type customError struct {
status 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())
}
diff --git a/examples/app-engine/README.md b/examples/app-engine/README.md
index 48505de8..b3dd7c78 100644
--- a/examples/app-engine/README.md
+++ b/examples/app-engine/README.md
@@ -1,7 +1,8 @@
# Guide to run Gin under App Engine LOCAL Development Server
1. Download, install and setup Go in your computer. (That includes setting your `$GOPATH`.)
-2. Download SDK for your platform from here: `https://developers.google.com/appengine/downloads?hl=es#Google_App_Engine_SDK_for_Go`
+2. Download SDK for your platform from [here](https://cloud.google.com/appengine/docs/standard/go/download): `https://cloud.google.com/appengine/docs/standard/go/download`
3. Download Gin source code using: `$ go get github.com/gin-gonic/gin`
-4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/`
-5. Run it: `$ goapp serve app-engine/`
\ No newline at end of file
+4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/app-engine/`
+5. Run it: `$ dev_appserver.py .` (notice that you have to run this script by Python2)
+
diff --git a/examples/assets-in-binary/README.md b/examples/assets-in-binary/README.md
new file mode 100644
index 00000000..0c23bb0d
--- /dev/null
+++ b/examples/assets-in-binary/README.md
@@ -0,0 +1,33 @@
+# Building a single binary containing templates
+
+This is a complete example to create a single binary with the
+[gin-gonic/gin][gin] Web Server with HTML templates.
+
+[gin]: https://github.com/gin-gonic/gin
+
+## How to use
+
+### Prepare Packages
+
+```
+go get github.com/gin-gonic/gin
+go get github.com/jessevdk/go-assets-builder
+```
+
+### Generate assets.go
+
+```
+go-assets-builder html -o assets.go
+```
+
+### Build the server
+
+```
+go build -o assets-in-binary
+```
+
+### Run
+
+```
+./assets-in-binary
+```
diff --git a/examples/assets-in-binary/assets.go b/examples/assets-in-binary/assets.go
new file mode 100644
index 00000000..dcc5c465
--- /dev/null
+++ b/examples/assets-in-binary/assets.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "time"
+
+ "github.com/jessevdk/go-assets"
+)
+
+var _Assetsbfa8d115ce0617d89507412d5393a462f8e9b003 = "\n\n