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 @@ [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) - [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) - [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) - [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) - [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) +[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) +[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) +[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) +[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin) +[![Release](https://img.shields.io/github/release/gin-gonic/gin.svg?style=flat-square)](https://github.com/gin-gonic/gin/releases) Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. ![Gin console logger](https://gin-gonic.github.io/gin/other/console.png) +## 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

Check some colors

- + - + - - + +
``` @@ -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 [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](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) { "bar", "foo") } +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, "bar", 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, "bar", 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

Can you see this? → {{.Bar}}

\n\n" +var _Assets3737a75b5254ed1f6d588b40a3449721f9ea86c2 = "\n\n

Hello, {{.Foo}}

\n\n" + +// Assets returns go-assets FileSystem +var Assets = assets.NewFileSystem(map[string][]string{"/": {"html"}, "/html": {"bar.tmpl", "index.tmpl"}}, map[string]*assets.File{ + "/": { + Path: "/", + FileMode: 0x800001ed, + Mtime: time.Unix(1524365738, 1524365738517125470), + Data: nil, + }, "/html": { + Path: "/html", + FileMode: 0x800001ed, + Mtime: time.Unix(1524365491, 1524365491289799093), + Data: nil, + }, "/html/bar.tmpl": { + Path: "/html/bar.tmpl", + FileMode: 0x1a4, + Mtime: time.Unix(1524365491, 1524365491289611557), + Data: []byte(_Assetsbfa8d115ce0617d89507412d5393a462f8e9b003), + }, "/html/index.tmpl": { + Path: "/html/index.tmpl", + FileMode: 0x1a4, + Mtime: time.Unix(1524365491, 1524365491289995821), + Data: []byte(_Assets3737a75b5254ed1f6d588b40a3449721f9ea86c2), + }}, "") diff --git a/examples/assets-in-binary/html/bar.tmpl b/examples/assets-in-binary/html/bar.tmpl new file mode 100644 index 00000000..c8e1c0ff --- /dev/null +++ b/examples/assets-in-binary/html/bar.tmpl @@ -0,0 +1,4 @@ + + +

Can you see this? → {{.Bar}}

+ diff --git a/examples/assets-in-binary/html/index.tmpl b/examples/assets-in-binary/html/index.tmpl new file mode 100644 index 00000000..6904fd58 --- /dev/null +++ b/examples/assets-in-binary/html/index.tmpl @@ -0,0 +1,4 @@ + + +

Hello, {{.Foo}}

+ diff --git a/examples/assets-in-binary/main.go b/examples/assets-in-binary/main.go new file mode 100644 index 00000000..27bc3b17 --- /dev/null +++ b/examples/assets-in-binary/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "html/template" + "io/ioutil" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +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", gin.H{ + "Foo": "World", + }) + }) + r.GET("/bar", func(c *gin.Context) { + c.HTML(http.StatusOK, "/html/bar.tmpl", gin.H{ + "Bar": "World", + }) + }) + r.Run(":8080") +} + +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 +} diff --git a/examples/basic/main.go b/examples/basic/main.go index 473c6a09..1c9e0ac4 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -1,10 +1,12 @@ package main import ( + "net/http" + "github.com/gin-gonic/gin" ) -var DB = make(map[string]string) +var db = make(map[string]string) func setupRouter() *gin.Engine { // Disable Console Color @@ -13,17 +15,17 @@ func setupRouter() *gin.Engine { // Ping test r.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") + c.String(http.StatusOK, "pong") }) // Get user value r.GET("/user/:name", func(c *gin.Context) { user := c.Params.ByName("name") - value, ok := DB[user] + value, ok := db[user] if ok { - c.JSON(200, gin.H{"user": user, "value": value}) + c.JSON(http.StatusOK, gin.H{"user": user, "value": value}) } else { - c.JSON(200, gin.H{"user": user, "status": "no value"}) + c.JSON(http.StatusOK, gin.H{"user": user, "status": "no value"}) } }) @@ -48,8 +50,8 @@ func setupRouter() *gin.Engine { } if c.Bind(&json) == nil { - DB[user] = json.Value - c.JSON(200, gin.H{"status": "ok"}) + db[user] = json.Value + c.JSON(http.StatusOK, gin.H{"status": "ok"}) } }) diff --git a/examples/basic/main_test.go b/examples/basic/main_test.go index 61203d66..5eb85240 100644 --- a/examples/basic/main_test.go +++ b/examples/basic/main_test.go @@ -15,6 +15,6 @@ func TestPingRoute(t *testing.T) { req, _ := http.NewRequest("GET", "/ping", nil) router.ServeHTTP(w, req) - assert.Equal(t, 200, w.Code) + assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "pong", w.Body.String()) } diff --git a/examples/custom-validation/server.go b/examples/custom-validation/server.go index 31d449f0..9238200b 100644 --- a/examples/custom-validation/server.go +++ b/examples/custom-validation/server.go @@ -10,6 +10,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"` @@ -30,7 +31,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") } diff --git a/examples/favicon/main.go b/examples/favicon/main.go index 5ad39331..d32ca098 100644 --- a/examples/favicon/main.go +++ b/examples/favicon/main.go @@ -1,6 +1,8 @@ package main import ( + "net/http" + "github.com/gin-gonic/gin" "github.com/thinkerou/favicon" ) @@ -9,7 +11,7 @@ func main() { app := gin.Default() app.Use(favicon.New("./favicon.ico")) app.GET("/ping", func(c *gin.Context) { - c.String(200, "Hello favicon.") + c.String(http.StatusOK, "Hello favicon.") }) app.Run(":8080") } diff --git a/examples/graceful-shutdown/graceful-shutdown/server.go b/examples/graceful-shutdown/graceful-shutdown/server.go index 6debe7f5..999a209e 100644 --- a/examples/graceful-shutdown/graceful-shutdown/server.go +++ b/examples/graceful-shutdown/graceful-shutdown/server.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "os/signal" + "syscall" "time" "github.com/gin-gonic/gin" @@ -27,15 +28,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 ...") @@ -44,5 +48,10 @@ 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") } diff --git a/examples/grpc/README.md b/examples/grpc/README.md new file mode 100644 index 00000000..a96d3c1c --- /dev/null +++ b/examples/grpc/README.md @@ -0,0 +1,19 @@ +## How to run this example + +1. run grpc server + +```sh +$ go run grpc/server.go +``` + +2. run gin server + +```sh +$ go run gin/main.go +``` + +3. use curl command to test it + +```sh +$ curl -v 'http://localhost:8052/rest/n/thinkerou' +``` diff --git a/examples/grpc/gin/main.go b/examples/grpc/gin/main.go new file mode 100644 index 00000000..820e65a3 --- /dev/null +++ b/examples/grpc/gin/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/gin-gonic/gin" + pb "github.com/gin-gonic/gin/examples/grpc/pb" + "google.golang.org/grpc" +) + +func main() { + // Set up a connection to the server. + conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) + if err != nil { + log.Fatalf("did not connect: %v", err) + } + defer conn.Close() + client := pb.NewGreeterClient(conn) + + // Set up a http server. + r := gin.Default() + r.GET("/rest/n/:name", func(c *gin.Context) { + name := c.Param("name") + + // Contact the server and print out its response. + req := &pb.HelloRequest{Name: name} + res, err := client.SayHello(c, req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "result": fmt.Sprint(res.Message), + }) + }) + + // Run http server + if err := r.Run(":8052"); err != nil { + log.Fatalf("could not run server: %v", err) + } +} diff --git a/examples/grpc/grpc/server.go b/examples/grpc/grpc/server.go new file mode 100644 index 00000000..d9bf9fc5 --- /dev/null +++ b/examples/grpc/grpc/server.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + "net" + + pb "github.com/gin-gonic/gin/examples/grpc/pb" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +// server is used to implement helloworld.GreeterServer. +type server struct{} + +// SayHello implements helloworld.GreeterServer +func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + return &pb.HelloReply{Message: "Hello " + in.Name}, nil +} + +func main() { + lis, err := net.Listen("tcp", ":50051") + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + s := grpc.NewServer() + pb.RegisterGreeterServer(s, &server{}) + + // Register reflection service on gRPC server. + reflection.Register(s) + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} diff --git a/examples/grpc/pb/helloworld.pb.go b/examples/grpc/pb/helloworld.pb.go new file mode 100644 index 00000000..c8c8942a --- /dev/null +++ b/examples/grpc/pb/helloworld.pb.go @@ -0,0 +1,151 @@ +// Code generated by protoc-gen-go. +// source: helloworld.proto +// DO NOT EDIT! + +/* +Package helloworld is a generated protocol buffer package. + +It is generated from these files: + helloworld.proto + +It has these top-level messages: + HelloRequest + HelloReply +*/ +package helloworld + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +// The request message containing the user's name. +type HelloRequest struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` +} + +func (m *HelloRequest) Reset() { *m = HelloRequest{} } +func (m *HelloRequest) String() string { return proto.CompactTextString(m) } +func (*HelloRequest) ProtoMessage() {} +func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +// The response message containing the greetings +type HelloReply struct { + Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"` +} + +func (m *HelloReply) Reset() { *m = HelloReply{} } +func (m *HelloReply) String() string { return proto.CompactTextString(m) } +func (*HelloReply) ProtoMessage() {} +func (*HelloReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func init() { + proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest") + proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for Greeter service + +type GreeterClient interface { + // Sends a greeting + SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) +} + +type greeterClient struct { + cc *grpc.ClientConn +} + +func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { + return &greeterClient{cc} +} + +func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { + out := new(HelloReply) + err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Greeter service + +type GreeterServer interface { + // Sends a greeting + SayHello(context.Context, *HelloRequest) (*HelloReply, error) +} + +func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { + s.RegisterService(&_Greeter_serviceDesc, srv) +} + +func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/helloworld.Greeter/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _Greeter_serviceDesc = grpc.ServiceDesc{ + ServiceName: "helloworld.Greeter", + HandlerType: (*GreeterServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _Greeter_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "helloworld.proto", +} + +func init() { proto.RegisterFile("helloworld.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 174 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9, + 0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88, + 0x28, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42, + 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92, + 0x1a, 0x17, 0x17, 0x54, 0x4d, 0x41, 0x4e, 0xa5, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71, + 0x62, 0x3a, 0x4c, 0x11, 0x8c, 0x6b, 0xe4, 0xc9, 0xc5, 0xee, 0x5e, 0x94, 0x9a, 0x5a, 0x92, 0x5a, + 0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64, + 0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x00, 0xad, 0x50, 0x62, 0x70, 0x32, 0xe0, 0x92, 0xce, 0xcc, 0xd7, + 0x4b, 0x2f, 0x2a, 0x48, 0xd6, 0x4b, 0xad, 0x48, 0xcc, 0x2d, 0xc8, 0x49, 0x2d, 0x46, 0x52, 0xeb, + 0xc4, 0x0f, 0x56, 0x1c, 0x0e, 0x62, 0x07, 0x80, 0xbc, 0x14, 0xc0, 0x98, 0xc4, 0x06, 0xf6, 0x9b, + 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xb7, 0xcd, 0xf2, 0xef, 0x00, 0x00, 0x00, +} diff --git a/examples/grpc/pb/helloworld.proto b/examples/grpc/pb/helloworld.proto new file mode 100644 index 00000000..d79a6a0d --- /dev/null +++ b/examples/grpc/pb/helloworld.proto @@ -0,0 +1,37 @@ +// Copyright 2015 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/examples/http-pusher/assets/app.js b/examples/http-pusher/assets/app.js new file mode 100644 index 00000000..05271b67 --- /dev/null +++ b/examples/http-pusher/assets/app.js @@ -0,0 +1 @@ +console.log("http2 pusher"); diff --git a/examples/http-pusher/main.go b/examples/http-pusher/main.go new file mode 100644 index 00000000..d4f33aa0 --- /dev/null +++ b/examples/http-pusher/main.go @@ -0,0 +1,41 @@ +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") +} diff --git a/examples/http-pusher/testdata/ca.pem b/examples/http-pusher/testdata/ca.pem new file mode 100644 index 00000000..6c8511a7 --- /dev/null +++ b/examples/http-pusher/testdata/ca.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla +Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 +YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT +BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 ++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu +g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd +Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau +sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m +oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG +Dfcog5wrJytaQ6UA0wE= +-----END CERTIFICATE----- diff --git a/examples/http-pusher/testdata/server.key b/examples/http-pusher/testdata/server.key new file mode 100644 index 00000000..143a5b87 --- /dev/null +++ b/examples/http-pusher/testdata/server.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD +M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf +3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY +AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm +V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY +tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p +dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q +K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR +81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff +DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd +aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2 +ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3 +XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe +F98XJ7tIFfJq +-----END PRIVATE KEY----- diff --git a/examples/http-pusher/testdata/server.pem b/examples/http-pusher/testdata/server.pem new file mode 100644 index 00000000..f3d43fcc --- /dev/null +++ b/examples/http-pusher/testdata/server.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET +MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx +MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV +BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 +ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco +LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg +zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd +9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy +em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G +CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 +hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh +y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 +-----END CERTIFICATE----- diff --git a/examples/http2/main.go b/examples/http2/main.go index 07df01e2..6598a4c9 100644 --- a/examples/http2/main.go +++ b/examples/http2/main.go @@ -3,6 +3,7 @@ package main import ( "html/template" "log" + "net/http" "os" "github.com/gin-gonic/gin" @@ -27,7 +28,7 @@ func main() { r.SetHTMLTemplate(html) r.GET("/welcome", func(c *gin.Context) { - c.HTML(200, "https", gin.H{ + c.HTML(http.StatusOK, "https", gin.H{ "status": "success", }) }) diff --git a/examples/new_relic/README.md b/examples/new_relic/README.md new file mode 100644 index 00000000..70f14942 --- /dev/null +++ b/examples/new_relic/README.md @@ -0,0 +1,30 @@ +The [New Relic Go Agent](https://github.com/newrelic/go-agent) provides a nice middleware for the stdlib handler signature. +The following is an adaptation of that middleware for Gin. + +```golang +const ( + // NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context + NewRelicTxnKey = "NewRelicTxnKey" +) + +// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler +func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc { + return func(ctx *gin.Context) { + txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request) + defer txn.End() + ctx.Set(NewRelicTxnKey, txn) + ctx.Next() + } +} +``` +and in `main.go` or equivalent... +```golang +router := gin.Default() +cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY")) +app, err := newrelic.NewApplication(cfg) +if err != nil { + log.Printf("failed to make new_relic app: %v", err) +} else { + router.Use(adapters.NewRelicMonitoring(app)) +} + ``` diff --git a/examples/new_relic/main.go b/examples/new_relic/main.go new file mode 100644 index 00000000..f85f7831 --- /dev/null +++ b/examples/new_relic/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "log" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "github.com/newrelic/go-agent" +) + +const ( + // NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context + NewRelicTxnKey = "NewRelicTxnKey" +) + +// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler +func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc { + return func(ctx *gin.Context) { + txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request) + defer txn.End() + ctx.Set(NewRelicTxnKey, txn) + ctx.Next() + } +} + +func main() { + router := gin.Default() + + cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY")) + app, err := newrelic.NewApplication(cfg) + if err != nil { + log.Printf("failed to make new_relic app: %v", err) + } else { + router.Use(NewRelicMonitoring(app)) + } + + router.GET("/", func(c *gin.Context) { + c.String(http.StatusOK, "Hello World!\n") + }) + router.Run() +} diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go index 1f3c8585..f3ead476 100644 --- a/examples/realtime-advanced/main.go +++ b/examples/realtime-advanced/main.go @@ -13,16 +13,19 @@ func main() { StartGin() } +// ConfigRuntime sets the number of operating system threads. func ConfigRuntime() { nuCPU := runtime.NumCPU() runtime.GOMAXPROCS(nuCPU) fmt.Printf("Running with %d CPUs\n", nuCPU) } +// StartWorkers start starsWorker by goroutine. func StartWorkers() { go statsWorker() } +// StartGin starts gin web server with setting router. func StartGin() { gin.SetMode(gin.ReleaseMode) diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html index 27dac387..905c012f 100644 --- a/examples/realtime-advanced/resources/room_login.templ.html +++ b/examples/realtime-advanced/resources/room_login.templ.html @@ -1,4 +1,4 @@ - + @@ -20,9 +20,9 @@ - + -