diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000000..9d49aa4126
--- /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 0000000000..8630bc3561
--- /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/.travis.yml b/.travis.yml
index e910156853..2eeb0b3de2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,13 +6,25 @@ go:
- 1.8.x
- 1.9.x
- 1.10.x
+ - 1.11.x
- master
+matrix:
+ fast_finish: true
+ include:
+ - go: 1.11.x
+ env: GO111MODULE=on
+
git:
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 7ab7213d98..dda19bcf3e 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 ee485ec3b5..e6a108ca32 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 5468563a0f..b698ac09e1 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,14 @@ 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; \
+ if [ -f profile.out ]; then \
+ cat profile.out | grep -v "mode:" >> coverage.out; \
+ rm profile.out; \
+ fi; \
+ done
.PHONY: fmt
fmt:
@@ -17,7 +27,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 +35,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 +51,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 51a807b5a9..1b5fb49353 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,13 @@
[![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.
@@ -15,10 +17,11 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
## Contents
+- [Installation](#installation)
+- [Prerequisite](#prerequisite)
- [Quick start](#quick-start)
- [Benchmarks](#benchmarks)
- [Gin v1.stable](#gin-v1-stable)
-- [Start using it](#start-using-it)
- [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)
@@ -26,6 +29,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [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)
@@ -37,7 +41,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- - [XML, JSON and YAML rendering](#xml-json-and-yaml-rendering)
+ - [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)
@@ -54,8 +58,69 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [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--)
+- [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
@@ -134,61 +199,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 .
@@ -228,7 +241,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)
@@ -315,6 +328,34 @@ 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
@@ -496,10 +537,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`
- **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`
- **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`.
@@ -509,8 +550,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() {
@@ -519,30 +560,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
@@ -572,6 +638,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).
@@ -590,6 +660,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"`
@@ -637,7 +708,7 @@ $ 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 registed this way.
+[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
@@ -683,9 +754,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"`
@@ -804,7 +878,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() {
@@ -838,6 +912,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")
}
@@ -888,6 +975,57 @@ func main() {
}
```
+#### 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
```go
@@ -1052,7 +1190,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{}{
@@ -1082,14 +1220,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
@@ -1572,11 +1722,11 @@ type StructX struct {
}
type StructY struct {
- Y StructX `form:"name_y"` // HERE hava form
+ Y StructX `form:"name_y"` // HERE have form
}
type StructZ struct {
- Z *StructZ `form:"name_z"` // HERE hava form
+ Z *StructZ `form:"name_z"` // HERE have form
}
```
@@ -1640,6 +1790,127 @@ enough to call binding at once.
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.
@@ -1686,9 +1957,11 @@ 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.
diff --git a/auth.go b/auth.go
index c2143091e6..9ed81b5dc5 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 dc8523b0b0..197e9208bf 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 e797003439..0b3f82df7d 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 1a98477739..3a2aad9cce 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -4,10 +4,9 @@
package binding
-import (
- "net/http"
-)
+import "net/http"
+// Content-Type MIME of the most common data formats.
const (
MIMEJSON = "application/json"
MIMEHTML = "text/html"
diff --git a/binding/binding_body_test.go b/binding/binding_body_test.go
index c41d9f86a1..dfd761e10d 100644
--- a/binding/binding_body_test.go
+++ b/binding/binding_body_test.go
@@ -5,7 +5,7 @@ import (
"io/ioutil"
"testing"
- "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"
@@ -55,12 +55,12 @@ func msgPackBody(t *testing.T) string {
}
func TestBindingBodyProto(t *testing.T) {
- test := example.Test{
+ test := protoexample.Test{
Label: proto.String("FOO"),
}
data, _ := proto.Marshal(&test)
req := requestWithBody("POST", "/", string(data))
- form := example.Test{}
+ 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 936deac7e0..efe87669d8 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -14,7 +14,7 @@ import (
"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"
@@ -562,7 +562,7 @@ func TestBindingFormMultipartFail(t *testing.T) {
}
func TestBindingProtoBuf(t *testing.T) {
- test := &example.Test{
+ test := &protoexample.Test{
Label: proto.String("yes"),
}
data, _ := proto.Marshal(test)
@@ -574,7 +574,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)
@@ -1156,14 +1156,14 @@ 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, 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, "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)
@@ -1179,7 +1179,7 @@ func (h hook) Read([]byte) (int, error) {
func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
- obj := example.Test{}
+ obj := protoexample.Test{}
req := requestWithBody("POST", path, body)
req.Body = ioutil.NopCloser(&hook{})
@@ -1187,7 +1187,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)
diff --git a/binding/default_validator.go b/binding/default_validator.go
index c67aa8a384..e7a302de67 100644
--- a/binding/default_validator.go
+++ b/binding/default_validator.go
@@ -18,11 +18,17 @@ 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
@@ -43,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_mapping.go b/binding/form_mapping.go
index 3f6b9bfa1a..f46a0dc127 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -74,16 +74,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
@@ -178,7 +178,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 fea17bb2fd..310922c1bd 100644
--- a/binding/json.go
+++ b/binding/json.go
@@ -9,7 +9,7 @@ import (
"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
diff --git a/context.go b/context.go
old mode 100755
new mode 100644
index 856e5d4e82..03fcf776f0
--- a/context.go
+++ b/context.go
@@ -159,16 +159,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
}
@@ -361,6 +360,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 {
@@ -403,7 +414,6 @@ 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 values := req.PostForm[key]; len(values) > 0 {
return values, true
@@ -416,8 +426,48 @@ 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
+ req.ParseMultipartForm(c.engine.MaxMultipartMemory)
+ 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
}
@@ -464,6 +514,11 @@ 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)
@@ -474,7 +529,7 @@ func (c *Context) BindQuery(obj interface{}) error {
// 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)
+ c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
}
return
@@ -498,6 +553,11 @@ 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)
@@ -539,14 +599,10 @@ func (c *Context) ShouldBindBodyWith(
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
}
@@ -593,9 +649,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
@@ -612,9 +668,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.
@@ -658,6 +714,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)
@@ -699,7 +756,12 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
// 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{}) {
- c.Render(code, render.JsonpJSON{Callback: c.DefaultQuery("callback", ""), Data: obj})
+ 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.
@@ -708,6 +770,12 @@ 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{}) {
@@ -719,6 +787,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})
@@ -764,6 +837,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()
@@ -785,6 +859,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
@@ -794,6 +869,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:
@@ -813,6 +889,7 @@ func (c *Context) Negotiate(code int, config Negotiate) {
}
}
+// NegotiateFormat returns an acceptable Accept format.
func (c *Context) NegotiateFormat(offered ...string) string {
assert1(len(offered) > 0, "you must provide at least one offer")
@@ -832,6 +909,7 @@ func (c *Context) NegotiateFormat(offered ...string) string {
return ""
}
+// SetAccepted sets Accept header data.
func (c *Context) SetAccepted(formats ...string) {
c.Accepted = formats
}
@@ -840,18 +918,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 0000000000..8e9f75ad7b
--- /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 0000000000..5b9ebcdc3b
--- /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 12e02fa056..fb492e0235 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"
@@ -19,8 +20,11 @@ import (
"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{}
@@ -47,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)
@@ -78,6 +84,19 @@ 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)
@@ -371,7 +390,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"))
@@ -439,6 +459,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) {
@@ -515,6 +559,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) {
@@ -541,10 +601,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 {
@@ -571,15 +632,16 @@ 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, "{\"foo\":\"bar\"}", w.Body.String())
- assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+ 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
@@ -589,11 +651,25 @@ func TestContextRenderJSONP(t *testing.T) {
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://example.com/?callback=x", nil)
- c.JSONP(201, H{"foo": "bar"})
+ c.JSONP(http.StatusCreated, H{"foo": "bar"})
- assert.Equal(t, 201, w.Code)
+ 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.HeaderMap.Get("Content-Type"))
+ assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
+}
+
+// Tests that the response is serialized as JSONP
+// 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.Header().Get("Content-Type"))
}
// Tests that no JSON is rendered if code is 204
@@ -601,11 +677,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
@@ -615,11 +691,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
@@ -628,11 +704,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
@@ -641,11 +717,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
@@ -653,11 +729,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
@@ -667,11 +743,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
@@ -679,11 +755,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, http.StatusNoContent, w.Code)
+ assert.Empty(t, w.Body.String())
+ 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, 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", w.Header().Get("Content-Type"))
}
// Tests that the response executes the templates
@@ -695,11 +782,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) {
@@ -710,20 +797,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(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
@@ -733,11 +820,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
@@ -746,11 +833,11 @@ func TestContextRenderXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
- c.XML(201, H{"foo": "bar"})
+ c.XML(http.StatusCreated, H{"foo": "bar"})
- assert.Equal(t, 201, w.Code)
+ assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "", w.Body.String())
- assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+ assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
// Tests that no XML is rendered if code is 204
@@ -758,11 +845,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
@@ -771,11 +858,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
@@ -783,11 +870,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
@@ -797,11 +884,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
@@ -810,11 +897,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`
@@ -823,11 +910,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
@@ -835,11 +922,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) {
@@ -866,9 +953,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
@@ -877,11 +964,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) {
@@ -909,9 +1020,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"))
}
@@ -920,10 +1031,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"))
}
@@ -932,21 +1043,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") })
}
@@ -955,14 +1068,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) {
@@ -970,14 +1083,14 @@ func TestContextNegotiationWithXML(t *testing.T) {
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
- c.Negotiate(200, Negotiate{
+ c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEXML, MIMEJSON},
Data: H{"foo": "bar"},
})
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "", w.Body.String())
- assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+ assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithHTML(t *testing.T) {
@@ -987,15 +1100,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) {
@@ -1003,11 +1116,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())
}
@@ -1031,7 +1144,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")
@@ -1065,11 +1178,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())
}
@@ -1087,11 +1200,11 @@ 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")
@@ -1154,9 +1267,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")
- assert.Equal(t, 401, w.Code)
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, abortIndex, c.index)
assert.True(t, c.IsAborted())
}
@@ -1230,6 +1343,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()
@@ -1264,7 +1397,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())
}
@@ -1300,6 +1433,27 @@ 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)
@@ -1486,7 +1640,62 @@ func TestContextRenderDataFromReader(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, body, w.Body.String())
- assert.Equal(t, contentType, w.HeaderMap.Get("Content-Type"))
- assert.Equal(t, fmt.Sprintf("%d", contentLength), w.HeaderMap.Get("Content-Length"))
- assert.Equal(t, extraHeaders["Content-Disposition"], w.HeaderMap.Get("Content-Disposition"))
+ 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
+ }()
+
+ w.Write([]byte("test"))
+
+ 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()
+ }()
+
+ writer.Write([]byte("test"))
+
+ return true
+ })
+
+ assert.Equal(t, "test", w.Body.String())
}
diff --git a/coverage.sh b/coverage.sh
deleted file mode 100644
index 81437f9183..0000000000
--- 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 897c4943c2..c5e65b2209 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,25 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
func debugPrint(format string, values ...interface{}) {
if IsDebugging() {
- log.Printf("[GIN-debug] "+format, values...)
+ 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 dfd54c8232..97ff166bff 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,121 @@ 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(func() {
+ SetMode(DebugMode)
+ SetMode(ReleaseMode)
+ debugPrint("DEBUG this!")
+ SetMode(TestMode)
+ debugPrint("DEBUG this!")
+ SetMode(DebugMode)
+ debugPrint("these are %d %s\n", 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(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(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(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(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(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(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(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()
+ io.Copy(&buf, reader)
+ 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/docs/how-to-build-an-effective-middleware.md b/docs/how-to-build-an-effective-middleware.md
new file mode 100644
index 0000000000..db04428c10
--- /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 initalize 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 furnction.
+
+```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 dbfccd856d..477b9d5827 100644
--- a/errors.go
+++ b/errors.go
@@ -9,21 +9,27 @@ 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 other any error.
ErrorTypeAny ErrorType = 1<<64 - 1
ErrorTypeNu = 2
)
+// Error represents a error's specification.
type Error struct {
Err error
Type ErrorType
@@ -34,11 +40,13 @@ 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
@@ -70,11 +78,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 +147,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 a666d7c1b1..9351b57893 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,17 +19,17 @@ 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))
@@ -38,22 +38,22 @@ func TestError(t *testing.T) {
"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{
"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
diff --git a/examples/basic/main.go b/examples/basic/main.go
index 473c6a09d9..1c9e0ac43c 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 61203d66f0..5eb8524033 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 dea0c302fd..9238200bad 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"`
diff --git a/examples/favicon/main.go b/examples/favicon/main.go
index 5ad39331d9..d32ca0985a 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/grpc/README.md b/examples/grpc/README.md
new file mode 100644
index 0000000000..a96d3c1ca1
--- /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 0000000000..edc1ca9be9
--- /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 setver.
+ 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 0000000000..d9bf9fc54c
--- /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 0000000000..c8c8942a12
--- /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 0000000000..d79a6a0d1f
--- /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 0000000000..05271b67cb
--- /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 0000000000..d4f33aa04d
--- /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 0000000000..6c8511a73c
--- /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 0000000000..143a5b8765
--- /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 0000000000..f3d43fcc5b
--- /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 07df01e2be..6598a4c9f8 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/realtime-advanced/main.go b/examples/realtime-advanced/main.go
index 1f3c8585ff..f3ead476f6 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/routes.go b/examples/realtime-advanced/routes.go
index 86da9bea22..03c6991085 100644
--- a/examples/realtime-advanced/routes.go
+++ b/examples/realtime-advanced/routes.go
@@ -4,6 +4,7 @@ import (
"fmt"
"html"
"io"
+ "net/http"
"strings"
"time"
@@ -21,12 +22,12 @@ func rateLimit(c *gin.Context) {
fmt.Println("ip blocked")
}
c.Abort()
- c.String(503, "you were automatically banned :)")
+ c.String(http.StatusServiceUnavailable, "you were automatically banned :)")
}
}
func index(c *gin.Context) {
- c.Redirect(301, "/room/hn")
+ c.Redirect(http.StatusMovedPermanently, "/room/hn")
}
func roomGET(c *gin.Context) {
@@ -38,7 +39,7 @@ func roomGET(c *gin.Context) {
if len(nick) > 13 {
nick = nick[0:12] + "..."
}
- c.HTML(200, "room_login.templ.html", gin.H{
+ c.HTML(http.StatusOK, "room_login.templ.html", gin.H{
"roomid": roomid,
"nick": nick,
"timestamp": time.Now().Unix(),
@@ -55,7 +56,7 @@ func roomPOST(c *gin.Context) {
validMessage := len(message) > 1 && len(message) < 200
validNick := len(nick) > 1 && len(nick) < 14
if !validMessage || !validNick {
- c.JSON(400, gin.H{
+ c.JSON(http.StatusBadRequest, gin.H{
"status": "failed",
"error": "the message or nickname is too long",
})
@@ -68,7 +69,7 @@ func roomPOST(c *gin.Context) {
}
messages.Add("inbound", 1)
room(roomid).Submit(post)
- c.JSON(200, post)
+ c.JSON(http.StatusOK, post)
}
func streamRoom(c *gin.Context) {
diff --git a/examples/realtime-advanced/stats.go b/examples/realtime-advanced/stats.go
index 4afedcb5d1..a6488035f4 100644
--- a/examples/realtime-advanced/stats.go
+++ b/examples/realtime-advanced/stats.go
@@ -50,6 +50,7 @@ func connectedUsers() uint64 {
return uint64(connected)
}
+// Stats returns savedStats data.
func Stats() map[string]uint64 {
mutexStats.RLock()
defer mutexStats.RUnlock()
diff --git a/examples/realtime-chat/main.go b/examples/realtime-chat/main.go
index e4b55a0f09..5741fcbadf 100644
--- a/examples/realtime-chat/main.go
+++ b/examples/realtime-chat/main.go
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"math/rand"
+ "net/http"
"github.com/gin-gonic/gin"
)
@@ -34,7 +35,7 @@ func stream(c *gin.Context) {
func roomGET(c *gin.Context) {
roomid := c.Param("roomid")
userid := fmt.Sprint(rand.Int31())
- c.HTML(200, "chat_room", gin.H{
+ c.HTML(http.StatusOK, "chat_room", gin.H{
"roomid": roomid,
"userid": userid,
})
@@ -46,7 +47,7 @@ func roomPOST(c *gin.Context) {
message := c.PostForm("message")
room(roomid).Submit(userid + ": " + message)
- c.JSON(200, gin.H{
+ c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": message,
})
diff --git a/examples/template/main.go b/examples/template/main.go
index f9e611dffa..e20a3b981e 100644
--- a/examples/template/main.go
+++ b/examples/template/main.go
@@ -20,7 +20,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{}{
diff --git a/gin.go b/gin.go
index 4205eff06f..440519f5f4 100644
--- a/gin.go
+++ b/gin.go
@@ -14,11 +14,7 @@ import (
"github.com/gin-gonic/gin/render"
)
-const (
- // Version is Framework's version.
- Version = "v1.2"
- defaultMultipartMemory = 32 << 20 // 32 MB
-)
+const defaultMultipartMemory = 32 << 20 // 32 MB
var (
default404Body = []byte("404 page not found")
@@ -26,7 +22,10 @@ var (
defaultAppEngine bool
)
+// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
+
+// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main own.
@@ -37,12 +36,15 @@ func (c HandlersChain) Last() HandlerFunc {
return nil
}
+// RouteInfo represents a request route's specification which contains method and path and its handler.
type RouteInfo struct {
- Method string
- Path string
- Handler string
+ Method string
+ Path string
+ Handler string
+ HandlerFunc HandlerFunc
}
+// RoutesInfo defines a RouteInfo array.
type RoutesInfo []RouteInfo
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
@@ -155,6 +157,7 @@ func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine}
}
+// Delims sets template left and right delims and returns a Engine instance.
func (engine *Engine) Delims(left, right string) *Engine {
engine.delims = render.Delims{Left: left, Right: right}
return engine
@@ -171,14 +174,14 @@ func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
func (engine *Engine) LoadHTMLGlob(pattern string) {
left := engine.delims.Left
right := engine.delims.Right
+ templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
if IsDebugging() {
- debugPrintLoadTemplate(template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern)))
+ debugPrintLoadTemplate(templ)
engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
return
}
- templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
engine.SetHTMLTemplate(templ)
}
@@ -264,10 +267,12 @@ func (engine *Engine) Routes() (routes RoutesInfo) {
func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo {
path += root.path
if len(root.handlers) > 0 {
+ handlerFunc := root.handlers.Last()
routes = append(routes, RouteInfo{
- Method: method,
- Path: path,
- Handler: nameOfFunction(root.handlers.Last()),
+ Method: method,
+ Path: path,
+ Handler: nameOfFunction(handlerFunc),
+ HandlerFunc: handlerFunc,
})
}
for _, child := range root.children {
@@ -329,12 +334,11 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
// HandleContext re-enter a context that has been rewritten.
-// This can be done by setting c.Request.Path to your new target.
+// This can be done by setting c.Request.URL.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
c.reset()
engine.handleHTTPRequest(c)
- engine.pool.Put(c)
}
func (engine *Engine) handleHTTPRequest(c *Context) {
@@ -349,43 +353,45 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
- if t[i].method == httpMethod {
- root := t[i].root
- // Find route in tree
- handlers, params, tsr := root.getValue(path, c.Params, unescape)
- if handlers != nil {
- c.handlers = handlers
- c.Params = params
- c.Next()
- c.writermem.WriteHeaderNow()
+ if t[i].method != httpMethod {
+ continue
+ }
+ root := t[i].root
+ // Find route in tree
+ handlers, params, tsr := root.getValue(path, c.Params, unescape)
+ if handlers != nil {
+ c.handlers = handlers
+ c.Params = params
+ c.Next()
+ c.writermem.WriteHeaderNow()
+ return
+ }
+ if httpMethod != "CONNECT" && path != "/" {
+ if tsr && engine.RedirectTrailingSlash {
+ redirectTrailingSlash(c)
return
}
- if httpMethod != "CONNECT" && path != "/" {
- if tsr && engine.RedirectTrailingSlash {
- redirectTrailingSlash(c)
- return
- }
- if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
- return
- }
+ if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
+ return
}
- break
}
+ break
}
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
- if tree.method != httpMethod {
- if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
- c.handlers = engine.allNoMethod
- serveError(c, 405, default405Body)
- return
- }
+ if tree.method == httpMethod {
+ continue
+ }
+ if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
+ c.handlers = engine.allNoMethod
+ serveError(c, http.StatusMethodNotAllowed, default405Body)
+ return
}
}
}
c.handlers = engine.allNoRoute
- serveError(c, 404, default404Body)
+ serveError(c, http.StatusNotFound, default404Body)
}
var mimePlain = []string{MIMEPlain}
@@ -393,28 +399,29 @@ var mimePlain = []string{MIMEPlain}
func serveError(c *Context, code int, defaultMessage []byte) {
c.writermem.status = code
c.Next()
- if !c.writermem.Written() {
- if c.writermem.Status() == code {
- c.writermem.Header()["Content-Type"] = mimePlain
- c.Writer.Write(defaultMessage)
- } else {
- c.writermem.WriteHeaderNow()
- }
+ if c.writermem.Written() {
+ return
}
+ if c.writermem.Status() == code {
+ c.writermem.Header()["Content-Type"] = mimePlain
+ c.Writer.Write(defaultMessage)
+ return
+ }
+ c.writermem.WriteHeaderNow()
+ return
}
func redirectTrailingSlash(c *Context) {
req := c.Request
path := req.URL.Path
- code := 301 // Permanent redirect, request with GET method
+ code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
- code = 307
+ code = http.StatusTemporaryRedirect
}
+ req.URL.Path = path + "/"
if length := len(path); length > 1 && path[length-1] == '/' {
req.URL.Path = path[:length-1]
- } else {
- req.URL.Path = path + "/"
}
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code)
@@ -425,14 +432,10 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request
path := req.URL.Path
- fixedPath, found := root.findCaseInsensitivePath(
- cleanPath(path),
- trailingSlash,
- )
- if found {
- code := 301 // Permanent redirect, request with GET method
+ if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(path), trailingSlash); ok {
+ code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
- code = 307
+ code = http.StatusTemporaryRedirect
}
req.URL.Path = string(fixedPath)
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
diff --git a/ginS/gins.go b/ginS/gins.go
index ee00b38163..04cf131c8c 100644
--- a/ginS/gins.go
+++ b/ginS/gins.go
@@ -22,14 +22,17 @@ func engine() *gin.Engine {
return internalEngine
}
+// LoadHTMLGlob is a wrapper for Engine.LoadHTMLGlob.
func LoadHTMLGlob(pattern string) {
engine().LoadHTMLGlob(pattern)
}
+// LoadHTMLFiles is a wrapper for Engine.LoadHTMLFiles.
func LoadHTMLFiles(files ...string) {
engine().LoadHTMLFiles(files...)
}
+// SetHTMLTemplate is a wrapper for Engine.SetHTMLTemplate.
func SetHTMLTemplate(templ *template.Template) {
engine().SetHTMLTemplate(templ)
}
@@ -39,7 +42,7 @@ func NoRoute(handlers ...gin.HandlerFunc) {
engine().NoRoute(handlers...)
}
-// NoMethod sets the handlers called when... TODO
+// NoMethod is a wrapper for Engine.NoMethod.
func NoMethod(handlers ...gin.HandlerFunc) {
engine().NoMethod(handlers...)
}
@@ -50,6 +53,7 @@ func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
return engine().Group(relativePath, handlers...)
}
+// Handle is a wrapper for Engine.Handle.
func Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().Handle(httpMethod, relativePath, handlers...)
}
@@ -89,10 +93,12 @@ func HEAD(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().HEAD(relativePath, handlers...)
}
+// Any is a wrapper for Engine.Any.
func Any(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().Any(relativePath, handlers...)
}
+// StaticFile is a wrapper for Engine.StaticFile.
func StaticFile(relativePath, filepath string) gin.IRoutes {
return engine().StaticFile(relativePath, filepath)
}
@@ -107,6 +113,7 @@ func Static(relativePath, root string) gin.IRoutes {
return engine().Static(relativePath, root)
}
+// StaticFS is a wrapper for Engine.StaticFS.
func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes {
return engine().StaticFS(relativePath, fs)
}
@@ -128,7 +135,7 @@ func Run(addr ...string) (err error) {
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens.
-func RunTLS(addr string, certFile string, keyFile string) (err error) {
+func RunTLS(addr, certFile, keyFile string) (err error) {
return engine().RunTLS(addr, certFile, keyFile)
}
diff --git a/gin_integration_test.go b/gin_integration_test.go
index 52f7884238..038c8b7c91 100644
--- a/gin_integration_test.go
+++ b/gin_integration_test.go
@@ -6,12 +6,14 @@ package gin
import (
"bufio"
+ "crypto/tls"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"os"
+ "sync"
"testing"
"time"
@@ -19,7 +21,14 @@ import (
)
func testRequest(t *testing.T, url string) {
- resp, err := http.Get(url)
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ }
+ client := &http.Client{Transport: tr}
+
+ resp, err := client.Get(url)
assert.NoError(t, err)
defer resp.Body.Close()
@@ -44,6 +53,22 @@ func TestRunEmpty(t *testing.T) {
testRequest(t, "http://localhost:8080/example")
}
+func TestRunTLS(t *testing.T) {
+ router := New()
+ go func() {
+ router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+
+ assert.NoError(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
+ }()
+
+ // have to wait for the goroutine to start and run the server
+ // otherwise the main thread will complete
+ time.Sleep(5 * time.Millisecond)
+
+ assert.Error(t, router.RunTLS(":8443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem"))
+ testRequest(t, "https://localhost:8443/example")
+}
+
func TestRunEmptyWithEnv(t *testing.T) {
os.Setenv("PORT", "3123")
router := New()
@@ -119,6 +144,29 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
testRequest(t, ts.URL+"/example")
}
+func TestConcurrentHandleContext(t *testing.T) {
+ router := New()
+ router.GET("/", func(c *Context) {
+ c.Request.URL.Path = "/example"
+ router.HandleContext(c)
+ })
+ router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+
+ ts := httptest.NewServer(router)
+ defer ts.Close()
+
+ var wg sync.WaitGroup
+ iterations := 200
+ wg.Add(iterations)
+ for i := 0; i < iterations; i++ {
+ go func() {
+ testRequest(t, ts.URL+"/")
+ wg.Done()
+ }()
+ }
+ wg.Wait()
+}
+
// func TestWithHttptestWithSpecifiedPort(t *testing.T) {
// router := New()
// router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
diff --git a/gin_test.go b/gin_test.go
index 3ac60577ea..353c9be13c 100644
--- a/gin_test.go
+++ b/gin_test.go
@@ -10,6 +10,7 @@ import (
"html/template"
"io/ioutil"
"net/http"
+ "net/http/httptest"
"reflect"
"testing"
"time"
@@ -22,105 +23,105 @@ func formatAsDate(t time.Time) string {
return fmt.Sprintf("%d/%02d/%02d", year, month, day)
}
-func setupHTMLFiles(t *testing.T, mode string, tls bool) func() {
- go func() {
- SetMode(mode)
- router := New()
- router.Delims("{[{", "}]}")
- router.SetFuncMap(template.FuncMap{
- "formatAsDate": formatAsDate,
- })
- router.LoadHTMLFiles("./fixtures/basic/hello.tmpl", "./fixtures/basic/raw.tmpl")
- router.GET("/test", func(c *Context) {
- c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
- })
- router.GET("/raw", func(c *Context) {
- c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
- "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
- })
+func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine)) *httptest.Server {
+ SetMode(mode)
+ router := New()
+ router.Delims("{[{", "}]}")
+ router.SetFuncMap(template.FuncMap{
+ "formatAsDate": formatAsDate,
+ })
+ loadMethod(router)
+ router.GET("/test", func(c *Context) {
+ c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
+ })
+ router.GET("/raw", func(c *Context) {
+ c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
+ "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
- if tls {
- // these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
- router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem")
- } else {
- router.Run(":8888")
- }
- }()
- t.Log("waiting 1 second for server startup")
- time.Sleep(1 * time.Second)
- return func() {}
-}
+ })
-func setupHTMLGlob(t *testing.T, mode string, tls bool) func() {
- go func() {
- SetMode(mode)
- router := New()
- router.Delims("{[{", "}]}")
- router.SetFuncMap(template.FuncMap{
- "formatAsDate": formatAsDate,
- })
- router.LoadHTMLGlob("./fixtures/basic/*")
- router.GET("/test", func(c *Context) {
- c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
- })
- router.GET("/raw", func(c *Context) {
- c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
- "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
- })
- })
- if tls {
- // these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
- router.RunTLS(":9999", "./fixtures/testdata/cert.pem", "./fixtures/testdata/key.pem")
- } else {
- router.Run(":8888")
- }
- }()
- t.Log("waiting 1 second for server startup")
- time.Sleep(1 * time.Second)
- return func() {}
+ var ts *httptest.Server
+
+ if tls {
+ ts = httptest.NewTLSServer(router)
+ } else {
+ ts = httptest.NewServer(router)
+ }
+
+ return ts
}
-func TestLoadHTMLGlob(t *testing.T) {
- td := setupHTMLGlob(t, DebugMode, false)
- res, err := http.Get("http://127.0.0.1:8888/test")
+func TestLoadHTMLGlobDebugMode(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ DebugMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLGlob("./testdata/template/*")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Hello world
", string(resp[:]))
-
- td()
+ assert.Equal(t, "Hello world
", string(resp))
}
-func TestLoadHTMLGlob2(t *testing.T) {
- td := setupHTMLGlob(t, TestMode, false)
- res, err := http.Get("http://127.0.0.1:8888/test")
+func TestLoadHTMLGlobTestMode(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ TestMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLGlob("./testdata/template/*")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Hello world
", string(resp[:]))
-
- td()
+ assert.Equal(t, "Hello world
", string(resp))
}
-func TestLoadHTMLGlob3(t *testing.T) {
- td := setupHTMLGlob(t, ReleaseMode, false)
- res, err := http.Get("http://127.0.0.1:8888/test")
+func TestLoadHTMLGlobReleaseMode(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ ReleaseMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLGlob("./testdata/template/*")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Hello world
", string(resp[:]))
-
- td()
+ assert.Equal(t, "Hello world
", string(resp))
}
func TestLoadHTMLGlobUsingTLS(t *testing.T) {
- td := setupHTMLGlob(t, DebugMode, true)
+ ts := setupHTMLFiles(
+ t,
+ DebugMode,
+ true,
+ func(router *Engine) {
+ router.LoadHTMLGlob("./testdata/template/*")
+ },
+ )
+ defer ts.Close()
+
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
@@ -128,29 +129,33 @@ func TestLoadHTMLGlobUsingTLS(t *testing.T) {
},
}
client := &http.Client{Transport: tr}
- res, err := client.Get("https://127.0.0.1:9999/test")
+ res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Hello world
", string(resp[:]))
-
- td()
+ assert.Equal(t, "Hello world
", string(resp))
}
func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
- time.Now()
- td := setupHTMLGlob(t, DebugMode, false)
- res, err := http.Get("http://127.0.0.1:8888/raw")
+ ts := setupHTMLFiles(
+ t,
+ DebugMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLGlob("./testdata/template/*")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Date: 2017/07/01\n", string(resp[:]))
-
- td()
+ assert.Equal(t, "Date: 2017/07/01\n", string(resp))
}
func init() {
@@ -164,59 +169,77 @@ func TestCreateEngine(t *testing.T) {
assert.Empty(t, router.Handlers)
}
-// func TestLoadHTMLDebugMode(t *testing.T) {
-// router := New()
-// SetMode(DebugMode)
-// router.LoadHTMLGlob("*.testtmpl")
-// r := router.HTMLRender.(render.HTMLDebug)
-// assert.Empty(t, r.Files)
-// assert.Equal(t, "*.testtmpl", r.Glob)
-//
-// router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl")
-// r = router.HTMLRender.(render.HTMLDebug)
-// assert.Empty(t, r.Glob)
-// assert.Equal(t, []string{"index.html", "login.html"}, r.Files)
-// SetMode(TestMode)
-// }
-
-func TestLoadHTMLFiles(t *testing.T) {
- td := setupHTMLFiles(t, TestMode, false)
- res, err := http.Get("http://127.0.0.1:8888/test")
+func TestLoadHTMLFilesTestMode(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ TestMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Hello world
", string(resp[:]))
- td()
+ assert.Equal(t, "Hello world
", string(resp))
}
-func TestLoadHTMLFiles2(t *testing.T) {
- td := setupHTMLFiles(t, DebugMode, false)
- res, err := http.Get("http://127.0.0.1:8888/test")
+func TestLoadHTMLFilesDebugMode(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ DebugMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Hello world
", string(resp[:]))
- td()
+ assert.Equal(t, "Hello world
", string(resp))
}
-func TestLoadHTMLFiles3(t *testing.T) {
- td := setupHTMLFiles(t, ReleaseMode, false)
- res, err := http.Get("http://127.0.0.1:8888/test")
+func TestLoadHTMLFilesReleaseMode(t *testing.T) {
+ ts := setupHTMLFiles(
+ t,
+ ReleaseMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Hello world
", string(resp[:]))
- td()
+ assert.Equal(t, "Hello world
", string(resp))
}
func TestLoadHTMLFilesUsingTLS(t *testing.T) {
- td := setupHTMLFiles(t, TestMode, true)
+ ts := setupHTMLFiles(
+ t,
+ TestMode,
+ true,
+ func(router *Engine) {
+ router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
@@ -224,28 +247,33 @@ func TestLoadHTMLFilesUsingTLS(t *testing.T) {
},
}
client := &http.Client{Transport: tr}
- res, err := client.Get("https://127.0.0.1:9999/test")
+ res, err := client.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Hello world
", string(resp[:]))
- td()
+ assert.Equal(t, "Hello world
", string(resp))
}
func TestLoadHTMLFilesFuncMap(t *testing.T) {
- time.Now()
- td := setupHTMLFiles(t, TestMode, false)
- res, err := http.Get("http://127.0.0.1:8888/raw")
+ ts := setupHTMLFiles(
+ t,
+ TestMode,
+ false,
+ func(router *Engine) {
+ router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
+ },
+ )
+ defer ts.Close()
+
+ res, err := http.Get(fmt.Sprintf("%s/raw", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
- assert.Equal(t, "Date: 2017/07/01\n", string(resp[:]))
-
- td()
+ assert.Equal(t, "Date: 2017/07/01\n", string(resp))
}
func TestAddRoute(t *testing.T) {
diff --git a/githubapi_test.go b/githubapi_test.go
index a08c264d77..f631035d23 100644
--- a/githubapi_test.go
+++ b/githubapi_test.go
@@ -293,7 +293,7 @@ func githubConfigRouter(router *Engine) {
for _, param := range c.Params {
output[param.Key] = param.Value
}
- c.JSON(200, output)
+ c.JSON(http.StatusOK, output)
})
}
}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000000..ef4103fd2e
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,30 @@
+module github.com/gin-gonic/gin
+
+require (
+ github.com/campoy/embedmd v0.0.0-20171205015432-c59ce00e0296
+ github.com/client9/misspell v0.3.4
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66
+ github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7
+ github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b
+ github.com/golang/protobuf v1.2.0
+ github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15
+ github.com/json-iterator/go v1.1.5
+ github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227
+ github.com/mattn/go-isatty v0.0.4
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/stretchr/testify v1.2.2
+ github.com/thinkerou/favicon v0.1.0
+ github.com/ugorji/go v1.1.1
+ golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e
+ golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd
+ golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1
+ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
+ golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba // indirect
+ google.golang.org/grpc v1.15.0
+ gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
+ gopkg.in/go-playground/validator.v8 v8.18.2
+ gopkg.in/yaml.v2 v2.2.1
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000000..2ef7f13b15
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,72 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/campoy/embedmd v0.0.0-20171205015432-c59ce00e0296 h1:tRsilif6pbtt+PX6uRoyGd+qR+4ZPucFZLHlc3Ak6z8=
+github.com/campoy/embedmd v0.0.0-20171205015432-c59ce00e0296/go.mod h1:/dBk8ICkslPCmyRdn4azP+QvBxL6Eg3EYxUGI9xMMFw=
+github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66 h1:QnnoVdChKs+GeTvN4rPYTW6b5U6M3HMEvQ/+x4IGtfY=
+github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66/go.mod h1:kTEh6M2J/mh7nsskr28alwLCXm/DSG5OSA/o31yy2XU=
+github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
+github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
+github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b h1:dm/NYytoj7p8Jc6zMvyRz3PCQrTTCXnVRvEzyBcM890=
+github.com/gin-gonic/autotls v0.0.0-20180426091246-be87bd5ef97b/go.mod h1:vwfeXwKgEIWq63oVfwaBjoByS4dZzYbHHROHjV4IjNY=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15 h1:cW/amwGEJK5MSKntPXRjX4dxs/nGxGT8gXKIsKFmHGc=
+github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15/go.mod h1:Fdm/oWRW+CH8PRbLntksCNtmcCBximKPkVQYvmMl80k=
+github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
+github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227 h1:KIaAZ/V+/0/6BOULrmBQ9T1ed8BkKqGIjIKW923nJuo=
+github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227/go.mod h1:ruMr5t05gVho4tuDv0PbI0Bb8nOxc/5Y6JzRHe/yfA0=
+github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/thinkerou/favicon v0.1.0 h1:eWMISKTpHq2G8HOuKn7ydD55j5DDehx94b0C2y8ABMs=
+github.com/thinkerou/favicon v0.1.0/go.mod h1:HL7Pap5kOluZv1ku34pZo/AJ44GaxMEPFZ3pmuexV2s=
+github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
+github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
+golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e h1:IzypfodbhbnViNUO/MEh0FzCUooG97cIGfdggUrUSyU=
+golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd h1:cgsAvzdqkDKdI02tIvDjO225vDPHMDCgfKqx5KEVI7U=
+golang.org/x/lint v0.0.0-20181011164241-5906bd5c48cd/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba h1:nZJIJPGow0Kf9bU9QTc1U6OXbs/7Hu4e+cNv+hxH+Zc=
+golang.org/x/sys v0.0.0-20181011152604-fa43e7bc11ba/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.15.0 h1:Az/KuahOM4NAidTEuJCv/RonAA7rYsTPkqXVjr+8OOw=
+google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
+gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal/json/json.go b/internal/json/json.go
new file mode 100644
index 0000000000..419d35f230
--- /dev/null
+++ b/internal/json/json.go
@@ -0,0 +1,20 @@
+// Copyright 2017 Bo-Yi Wu. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+// +build !jsoniter
+
+package json
+
+import "encoding/json"
+
+var (
+ // Marshal is exported by gin/json package.
+ Marshal = json.Marshal
+ // MarshalIndent is exported by gin/json package.
+ MarshalIndent = json.MarshalIndent
+ // NewDecoder is exported by gin/json package.
+ NewDecoder = json.NewDecoder
+ // NewEncoder is exported by gin/json package.
+ NewEncoder = json.NewEncoder
+)
diff --git a/internal/json/jsoniter.go b/internal/json/jsoniter.go
new file mode 100644
index 0000000000..2021c53c18
--- /dev/null
+++ b/internal/json/jsoniter.go
@@ -0,0 +1,21 @@
+// Copyright 2017 Bo-Yi Wu. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+// +build jsoniter
+
+package json
+
+import "github.com/json-iterator/go"
+
+var (
+ json = jsoniter.ConfigCompatibleWithStandardLibrary
+ // Marshal is exported by gin/json package.
+ Marshal = json.Marshal
+ // MarshalIndent is exported by gin/json package.
+ MarshalIndent = json.MarshalIndent
+ // NewDecoder is exported by gin/json package.
+ NewDecoder = json.NewDecoder
+ // NewEncoder is exported by gin/json package.
+ NewEncoder = json.NewEncoder
+)
diff --git a/json/json.go b/json/json.go
deleted file mode 100644
index aa76aa3098..0000000000
--- a/json/json.go
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2017 Bo-Yi Wu. All rights reserved.
-// Use of this source code is governed by a MIT style
-// license that can be found in the LICENSE file.
-
-// +build !jsoniter
-
-package json
-
-import "encoding/json"
-
-var (
- Marshal = json.Marshal
- MarshalIndent = json.MarshalIndent
- NewDecoder = json.NewDecoder
-)
diff --git a/json/jsoniter.go b/json/jsoniter.go
deleted file mode 100644
index ffe1424acd..0000000000
--- a/json/jsoniter.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2017 Bo-Yi Wu. All rights reserved.
-// Use of this source code is governed by a MIT style
-// license that can be found in the LICENSE file.
-
-// +build jsoniter
-
-package json
-
-import "github.com/json-iterator/go"
-
-var (
- json = jsoniter.ConfigCompatibleWithStandardLibrary
- Marshal = json.Marshal
- MarshalIndent = json.MarshalIndent
- NewDecoder = json.NewDecoder
-)
diff --git a/logger.go b/logger.go
index c679c78718..5f986853e6 100644
--- a/logger.go
+++ b/logger.go
@@ -7,6 +7,7 @@ package gin
import (
"fmt"
"io"
+ "net/http"
"os"
"time"
@@ -16,7 +17,7 @@ import (
var (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
- yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
+ yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
@@ -118,11 +119,11 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
func colorForStatus(code int) string {
switch {
- case code >= 200 && code < 300:
+ case code >= http.StatusOK && code < http.StatusMultipleChoices:
return green
- case code >= 300 && code < 400:
+ case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
return white
- case code >= 400 && code < 500:
+ case code >= http.StatusBadRequest && code < http.StatusInternalServerError:
return yellow
default:
return red
diff --git a/logger_test.go b/logger_test.go
index 74a9659c0b..6118cb0499 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -7,6 +7,7 @@ package gin
import (
"bytes"
"errors"
+ "net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -84,7 +85,7 @@ func TestLogger(t *testing.T) {
func TestColorForMethod(t *testing.T) {
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
- assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
+ assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta")
@@ -93,9 +94,9 @@ func TestColorForMethod(t *testing.T) {
}
func TestColorForStatus(t *testing.T) {
- assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(200), "2xx should be green")
- assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(301), "3xx should be white")
- assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForStatus(404), "4xx should be yellow")
+ assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
+ assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
+ assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
}
@@ -106,23 +107,23 @@ func TestErrorLogger(t *testing.T) {
c.Error(errors.New("this is an error"))
})
router.GET("/abort", func(c *Context) {
- c.AbortWithError(401, errors.New("no authorized"))
+ c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized"))
})
router.GET("/print", func(c *Context) {
c.Error(errors.New("this is an error"))
- c.String(500, "hola!")
+ c.String(http.StatusInternalServerError, "hola!")
})
w := performRequest(router, "GET", "/error")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort")
- assert.Equal(t, 401, w.Code)
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print")
- assert.Equal(t, 500, w.Code)
+ assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
}
diff --git a/middleware_test.go b/middleware_test.go
index aa6a37a89a..983ad9337f 100644
--- a/middleware_test.go
+++ b/middleware_test.go
@@ -6,6 +6,7 @@ package gin
import (
"errors"
+ "net/http"
"strings"
"testing"
@@ -37,7 +38,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
w := performRequest(router, "GET", "/")
// TEST
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ACDB", signature)
}
@@ -73,7 +74,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
w := performRequest(router, "GET", "/")
// TEST
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "ACEGHFDB", signature)
}
@@ -110,7 +111,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
w := performRequest(router, "GET", "/")
// TEST
- assert.Equal(t, 405, w.Code)
+ assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
assert.Equal(t, "ACEGHFDB", signature)
}
@@ -147,7 +148,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
w := performRequest(router, "GET", "/")
// TEST
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "AC X DB", signature)
}
@@ -159,7 +160,7 @@ func TestMiddlewareAbort(t *testing.T) {
})
router.Use(func(c *Context) {
signature += "C"
- c.AbortWithStatus(401)
+ c.AbortWithStatus(http.StatusUnauthorized)
c.Next()
signature += "D"
})
@@ -173,7 +174,7 @@ func TestMiddlewareAbort(t *testing.T) {
w := performRequest(router, "GET", "/")
// TEST
- assert.Equal(t, 401, w.Code)
+ assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "ACD", signature)
}
@@ -183,7 +184,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
router.Use(func(c *Context) {
signature += "A"
c.Next()
- c.AbortWithStatus(410)
+ c.AbortWithStatus(http.StatusGone)
signature += "B"
})
@@ -195,7 +196,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
w := performRequest(router, "GET", "/")
// TEST
- assert.Equal(t, 410, w.Code)
+ assert.Equal(t, http.StatusGone, w.Code)
assert.Equal(t, "ACB", signature)
}
@@ -207,7 +208,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
router := New()
router.Use(func(context *Context) {
signature += "A"
- context.AbortWithError(500, errors.New("foo"))
+ context.AbortWithError(http.StatusInternalServerError, errors.New("foo"))
})
router.Use(func(context *Context) {
signature += "B"
@@ -218,25 +219,25 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
w := performRequest(router, "GET", "/")
// TEST
- assert.Equal(t, 500, w.Code)
+ assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "A", signature)
}
func TestMiddlewareWrite(t *testing.T) {
router := New()
router.Use(func(c *Context) {
- c.String(400, "hola\n")
+ c.String(http.StatusBadRequest, "hola\n")
})
router.Use(func(c *Context) {
- c.XML(400, H{"foo": "bar"})
+ c.XML(http.StatusBadRequest, H{"foo": "bar"})
})
router.Use(func(c *Context) {
- c.JSON(400, H{"foo": "bar"})
+ c.JSON(http.StatusBadRequest, H{"foo": "bar"})
})
router.GET("/", func(c *Context) {
- c.JSON(400, H{"foo": "bar"})
+ c.JSON(http.StatusBadRequest, H{"foo": "bar"})
}, func(c *Context) {
- c.Render(400, sse.Event{
+ c.Render(http.StatusBadRequest, sse.Event{
Event: "test",
Data: "message",
})
@@ -244,6 +245,6 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/")
- assert.Equal(t, 400, w.Code)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.Replace("hola\n{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
}
diff --git a/mode.go b/mode.go
index 9df4e45fa8..7cb0143a43 100644
--- a/mode.go
+++ b/mode.go
@@ -11,12 +11,16 @@ import (
"github.com/gin-gonic/gin/binding"
)
+// ENV_GIN_MODE indicates environment name for gin mode.
const ENV_GIN_MODE = "GIN_MODE"
const (
- DebugMode = "debug"
+ // DebugMode indicates gin mode is debug.
+ DebugMode = "debug"
+ // ReleaseMode indicates gin mode is relase.
ReleaseMode = "release"
- TestMode = "test"
+ // TestMode indicates gin mode is test.
+ TestMode = "test"
)
const (
debugCode = iota
@@ -42,6 +46,7 @@ func init() {
SetMode(mode)
}
+// SetMode sets gin mode according to input string.
func SetMode(value string) {
switch value {
case DebugMode, "":
@@ -59,14 +64,18 @@ func SetMode(value string) {
modeName = value
}
+// DisableBindValidation closes the default validator.
func DisableBindValidation() {
binding.Validator = nil
}
+// EnableJsonDecoderUseNumber sets true for binding.EnableDecoderUseNumberto to
+// call the UseNumber method on the JSON Decoder instance.
func EnableJsonDecoderUseNumber() {
binding.EnableDecoderUseNumber = true
}
+// Mode returns currently gin mode.
func Mode() string {
return modeName
}
diff --git a/path.go b/path.go
index ed63ad1aa2..d1f5962286 100644
--- a/path.go
+++ b/path.go
@@ -41,7 +41,7 @@ func cleanPath(p string) string {
buf[0] = '/'
}
- trailing := n > 2 && p[n-1] == '/'
+ trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this
@@ -59,11 +59,11 @@ func cleanPath(p string) string {
case p[r] == '.' && p[r+1] == '/':
// . element
- r++
+ r += 2
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last /
- r += 2
+ r += 3
if w > 1 {
// can backtrack
diff --git a/path_test.go b/path_test.go
index 4a6d945b64..c1e6ed4f4e 100644
--- a/path_test.go
+++ b/path_test.go
@@ -24,6 +24,7 @@ var cleanTests = []struct {
// missing root
{"", "/"},
+ {"a/", "/a/"},
{"abc", "/abc"},
{"abc/def", "/abc/def"},
{"a/b/c", "/a/b/c"},
diff --git a/recovery.go b/recovery.go
index dcbeba74ec..6c28b4fadb 100644
--- a/recovery.go
+++ b/recovery.go
@@ -10,6 +10,7 @@ import (
"io"
"io/ioutil"
"log"
+ "net/http"
"net/http/httputil"
"runtime"
"time"
@@ -38,10 +39,14 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
if err := recover(); err != nil {
if logger != nil {
stack := stack(3)
- httprequest, _ := httputil.DumpRequest(c.Request, false)
- logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset)
+ if IsDebugging() {
+ httprequest, _ := httputil.DumpRequest(c.Request, false)
+ logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset)
+ } else {
+ logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset)
+ }
}
- c.AbortWithStatus(500)
+ c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
diff --git a/recovery_test.go b/recovery_test.go
index de3b62a50e..7d422b74f2 100644
--- a/recovery_test.go
+++ b/recovery_test.go
@@ -6,6 +6,7 @@ package gin
import (
"bytes"
+ "net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -22,10 +23,20 @@ func TestPanicInHandler(t *testing.T) {
// RUN
w := performRequest(router, "GET", "/recovery")
// TEST
- assert.Equal(t, 500, w.Code)
- assert.Contains(t, buffer.String(), "GET /recovery")
+ assert.Equal(t, http.StatusInternalServerError, w.Code)
+ assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "TestPanicInHandler")
+ assert.NotContains(t, buffer.String(), "GET /recovery")
+
+ // Debug mode prints the request
+ SetMode(DebugMode)
+ // RUN
+ w = performRequest(router, "GET", "/recovery")
+ // TEST
+ assert.Equal(t, http.StatusInternalServerError, w.Code)
+ assert.Contains(t, buffer.String(), "GET /recovery")
+
}
// TestPanicWithAbort assert that panic has been recovered even if context.Abort was used.
@@ -33,11 +44,31 @@ func TestPanicWithAbort(t *testing.T) {
router := New()
router.Use(RecoveryWithWriter(nil))
router.GET("/recovery", func(c *Context) {
- c.AbortWithStatus(400)
+ c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery")
// TEST
- assert.Equal(t, 400, w.Code)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+}
+
+func TestSource(t *testing.T) {
+ bs := source(nil, 0)
+ assert.Equal(t, []byte("???"), bs)
+
+ in := [][]byte{
+ []byte("Hello world."),
+ []byte("Hi, gin.."),
+ }
+ bs = source(in, 10)
+ assert.Equal(t, []byte("???"), bs)
+
+ bs = source(in, 1)
+ assert.Equal(t, []byte("Hello world."), bs)
+}
+
+func TestFunction(t *testing.T) {
+ bs := function(1)
+ assert.Equal(t, []byte("???"), bs)
}
diff --git a/render/data.go b/render/data.go
index 3319491397..6ba657ba0a 100644
--- a/render/data.go
+++ b/render/data.go
@@ -6,6 +6,7 @@ package render
import "net/http"
+// Data contains ContentType and bytes data.
type Data struct {
ContentType string
Data []byte
@@ -18,6 +19,7 @@ func (r Data) Render(w http.ResponseWriter) (err error) {
return
}
+// WriteContentType (Data) writes custom ContentType.
func (r Data) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType})
}
diff --git a/render/html.go b/render/html.go
index 1e3be65b9d..6696ece997 100644
--- a/render/html.go
+++ b/render/html.go
@@ -9,20 +9,27 @@ import (
"net/http"
)
+// Delims represents a set of Left and Right delimiters for HTML template rendering.
type Delims struct {
- Left string
+ // Left delimiter, defaults to {{.
+ Left string
+ // Right delimiter, defaults to }}.
Right string
}
+// HTMLRender interface is to be implemented by HTMLProduction and HTMLDebug.
type HTMLRender interface {
+ // Instance returns an HTML instance.
Instance(string, interface{}) Render
}
+// HTMLProduction contains template reference and its delims.
type HTMLProduction struct {
Template *template.Template
Delims Delims
}
+// HTMLDebug contains template delims and pattern and function with file list.
type HTMLDebug struct {
Files []string
Glob string
@@ -30,6 +37,7 @@ type HTMLDebug struct {
FuncMap template.FuncMap
}
+// HTML contains template reference and its name with given interface object.
type HTML struct {
Template *template.Template
Name string
@@ -38,6 +46,7 @@ type HTML struct {
var htmlContentType = []string{"text/html; charset=utf-8"}
+// Instance (HTMLProduction) returns an HTML instance which it realizes Render interface.
func (r HTMLProduction) Instance(name string, data interface{}) Render {
return HTML{
Template: r.Template,
@@ -46,6 +55,7 @@ func (r HTMLProduction) Instance(name string, data interface{}) Render {
}
}
+// Instance (HTMLDebug) returns an HTML instance which it realizes Render interface.
func (r HTMLDebug) Instance(name string, data interface{}) Render {
return HTML{
Template: r.loadTemplate(),
@@ -66,6 +76,7 @@ func (r HTMLDebug) loadTemplate() *template.Template {
panic("the HTML debug render was created without files or glob pattern")
}
+// Render (HTML) executes template and writes its result with custom ContentType for response.
func (r HTML) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
@@ -75,6 +86,7 @@ func (r HTML) Render(w http.ResponseWriter) error {
return r.Template.ExecuteTemplate(w, r.Name, r.Data)
}
+// WriteContentType (HTML) writes HTML ContentType.
func (r HTML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, htmlContentType)
}
diff --git a/render/json.go b/render/json.go
old mode 100755
new mode 100644
index 3a2e8b2fcf..32d0fc421a
--- a/render/json.go
+++ b/render/json.go
@@ -6,35 +6,48 @@ package render
import (
"bytes"
+ "fmt"
"html/template"
"net/http"
- "github.com/gin-gonic/gin/json"
+ "github.com/gin-gonic/gin/internal/json"
)
+// JSON contains the given interface object.
type JSON struct {
Data interface{}
}
+// IndentedJSON contains the given interface object.
type IndentedJSON struct {
Data interface{}
}
+// SecureJSON contains the given interface object and its prefix.
type SecureJSON struct {
Prefix string
Data interface{}
}
+// JsonpJSON contains the given interface object its callback.
type JsonpJSON struct {
Callback string
Data interface{}
}
+// AsciiJSON contains the given interface object.
+type AsciiJSON struct {
+ Data interface{}
+}
+
+// SecureJSONPrefix is a string which represents SecureJSON prefix.
type SecureJSONPrefix string
var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
+var jsonAsciiContentType = []string{"application/json"}
+// Render (JSON) writes data with custom ContentType.
func (r JSON) Render(w http.ResponseWriter) (err error) {
if err = WriteJSON(w, r.Data); err != nil {
panic(err)
@@ -42,10 +55,12 @@ func (r JSON) Render(w http.ResponseWriter) (err error) {
return
}
+// WriteContentType (JSON) writes JSON ContentType.
func (r JSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
}
+// WriteJSON marshals the given interface object and writes it with custom ContentType.
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj)
@@ -56,6 +71,7 @@ func WriteJSON(w http.ResponseWriter, obj interface{}) error {
return nil
}
+// Render (IndentedJSON) marshals the given interface object and writes it with custom ContentType.
func (r IndentedJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
@@ -66,10 +82,12 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error {
return nil
}
+// WriteContentType (IndentedJSON) writes JSON ContentType.
func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
}
+// Render (SecureJSON) marshals the given interface object and writes it with custom ContentType.
func (r SecureJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
jsonBytes, err := json.Marshal(r.Data)
@@ -84,10 +102,12 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
return nil
}
+// WriteContentType (SecureJSON) writes JSON ContentType.
func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
}
+// Render (JsonpJSON) marshals the given interface object and writes it and its callback with custom ContentType.
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
@@ -109,6 +129,33 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
return nil
}
+// WriteContentType (JsonpJSON) writes Javascript ContentType.
func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonpContentType)
}
+
+// Render (AsciiJSON) marshals the given interface object and writes it with custom ContentType.
+func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
+ r.WriteContentType(w)
+ ret, err := json.Marshal(r.Data)
+ if err != nil {
+ return err
+ }
+
+ var buffer bytes.Buffer
+ for _, r := range string(ret) {
+ cvt := string(r)
+ if r >= 128 {
+ cvt = fmt.Sprintf("\\u%04x", int64(r))
+ }
+ buffer.WriteString(cvt)
+ }
+
+ w.Write(buffer.Bytes())
+ return nil
+}
+
+// WriteContentType (AsciiJSON) writes JSON ContentType.
+func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, jsonAsciiContentType)
+}
diff --git a/render/json_17.go b/render/json_17.go
new file mode 100644
index 0000000000..208193c7e3
--- /dev/null
+++ b/render/json_17.go
@@ -0,0 +1,31 @@
+// Copyright 2018 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+// +build go1.7
+
+package render
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin/internal/json"
+)
+
+// PureJSON contains the given interface object.
+type PureJSON struct {
+ Data interface{}
+}
+
+// Render (PureJSON) writes custom ContentType and encodes the given interface object.
+func (r PureJSON) Render(w http.ResponseWriter) error {
+ r.WriteContentType(w)
+ encoder := json.NewEncoder(w)
+ encoder.SetEscapeHTML(false)
+ return encoder.Encode(r.Data)
+}
+
+// WriteContentType (PureJSON) writes custom ContentType.
+func (r PureJSON) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, jsonContentType)
+}
diff --git a/render/msgpack.go b/render/msgpack.go
index e6c13e5881..dc681fcf87 100644
--- a/render/msgpack.go
+++ b/render/msgpack.go
@@ -10,22 +10,26 @@ import (
"github.com/ugorji/go/codec"
)
+// MsgPack contains the given interface object.
type MsgPack struct {
Data interface{}
}
var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
+// WriteContentType (MsgPack) writes MsgPack ContentType.
func (r MsgPack) WriteContentType(w http.ResponseWriter) {
writeContentType(w, msgpackContentType)
}
+// Render (MsgPack) encodes the given interface object and writes data with custom ContentType.
func (r MsgPack) Render(w http.ResponseWriter) error {
return WriteMsgPack(w, r.Data)
}
+// WriteMsgPack writes MsgPack ContentType and encodes the given interface object.
func WriteMsgPack(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, msgpackContentType)
- var h codec.Handle = new(codec.MsgpackHandle)
- return codec.NewEncoder(w, h).Encode(obj)
+ var mh codec.MsgpackHandle
+ return codec.NewEncoder(w, &mh).Encode(obj)
}
diff --git a/render/protobuf.go b/render/protobuf.go
new file mode 100644
index 0000000000..4789507262
--- /dev/null
+++ b/render/protobuf.go
@@ -0,0 +1,36 @@
+// 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 render
+
+import (
+ "net/http"
+
+ "github.com/golang/protobuf/proto"
+)
+
+// ProtoBuf contains the given interface object.
+type ProtoBuf struct {
+ Data interface{}
+}
+
+var protobufContentType = []string{"application/x-protobuf"}
+
+// Render (ProtoBuf) marshals the given interface object and writes data with custom ContentType.
+func (r ProtoBuf) Render(w http.ResponseWriter) error {
+ r.WriteContentType(w)
+
+ bytes, err := proto.Marshal(r.Data.(proto.Message))
+ if err != nil {
+ return err
+ }
+
+ w.Write(bytes)
+ return nil
+}
+
+// WriteContentType (ProtoBuf) writes ProtoBuf ContentType.
+func (r ProtoBuf) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, protobufContentType)
+}
diff --git a/render/reader.go b/render/reader.go
index be2132c8fa..ab60e53acb 100644
--- a/render/reader.go
+++ b/render/reader.go
@@ -1,3 +1,7 @@
+// 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 render
import (
@@ -6,6 +10,7 @@ import (
"strconv"
)
+// Reader contains the IO reader and its length, and custom ContentType and other headers.
type Reader struct {
ContentType string
ContentLength int64
@@ -22,10 +27,12 @@ func (r Reader) Render(w http.ResponseWriter) (err error) {
return
}
+// WriteContentType (Reader) writes custom ContentType.
func (r Reader) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType})
}
+// writeHeaders writes custom Header.
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
header := w.Header()
for k, v := range headers {
diff --git a/render/redirect.go b/render/redirect.go
index f874a3515d..9c145fe276 100644
--- a/render/redirect.go
+++ b/render/redirect.go
@@ -9,13 +9,17 @@ import (
"net/http"
)
+// Redirect contains the http request reference and redirects status code and location.
type Redirect struct {
Code int
Request *http.Request
Location string
}
+// Render (Redirect) redirects the http request to new location and writes redirect response.
func (r Redirect) Render(w http.ResponseWriter) error {
+ // todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
+ // when we upgrade go version we can use http.StatusPermanentRedirect
if (r.Code < 300 || r.Code > 308) && r.Code != 201 {
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
}
@@ -23,4 +27,5 @@ func (r Redirect) Render(w http.ResponseWriter) error {
return nil
}
+// WriteContentType (Redirect) don't write any ContentType.
func (r Redirect) WriteContentType(http.ResponseWriter) {}
diff --git a/render/render.go b/render/render.go
old mode 100755
new mode 100644
index 7caf9bbab0..abfc79fcd6
--- a/render/render.go
+++ b/render/render.go
@@ -6,8 +6,11 @@ package render
import "net/http"
+// Render interface is to be implemented by JSON, XML, HTML, YAML and so on.
type Render interface {
+ // Render writes data with custom ContentType.
Render(http.ResponseWriter) error
+ // WriteContentType writes custom ContentType.
WriteContentType(w http.ResponseWriter)
}
@@ -26,6 +29,8 @@ var (
_ Render = YAML{}
_ Render = MsgPack{}
_ Render = Reader{}
+ _ Render = AsciiJSON{}
+ _ Render = ProtoBuf{}
)
func writeContentType(w http.ResponseWriter, value []string) {
diff --git a/render/render_17_test.go b/render/render_17_test.go
new file mode 100644
index 0000000000..68330090f6
--- /dev/null
+++ b/render/render_17_test.go
@@ -0,0 +1,26 @@
+// Copyright 2018 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+// +build go1.7
+
+package render
+
+import (
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRenderPureJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ data := map[string]interface{}{
+ "foo": "bar",
+ "html": "",
+ }
+ err := (PureJSON{data}).Render(w)
+ assert.NoError(t, err)
+ 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/render/render_test.go b/render/render_test.go
old mode 100755
new mode 100644
index 40ec806ecc..4c9b180d49
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -15,8 +15,11 @@ import (
"strings"
"testing"
+ "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
+
+ testdata "github.com/gin-gonic/gin/testdata/protoexample"
)
// TODO unit tests
@@ -49,7 +52,8 @@ func TestRenderMsgPack(t *testing.T) {
func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
- "foo": "bar",
+ "foo": "bar",
+ "html": "",
}
(JSON{data}).WriteContentType(w)
@@ -58,7 +62,7 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w)
assert.NoError(t, err)
- assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+ assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\\u003cb\\u003e\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
@@ -158,6 +162,21 @@ func TestRenderJsonpJSON(t *testing.T) {
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
}
+func TestRenderJsonpJSONError2(t *testing.T) {
+ w := httptest.NewRecorder()
+ data := map[string]interface{}{
+ "foo": "bar",
+ }
+ (JsonpJSON{"", data}).WriteContentType(w)
+ assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
+
+ e := (JsonpJSON{"", data}).Render(w)
+ assert.NoError(t, e)
+
+ assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+ assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
+}
+
func TestRenderJsonpJSONFail(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
@@ -167,6 +186,35 @@ func TestRenderJsonpJSONFail(t *testing.T) {
assert.Error(t, err)
}
+func TestRenderAsciiJSON(t *testing.T) {
+ w1 := httptest.NewRecorder()
+ data1 := map[string]interface{}{
+ "lang": "GO语言",
+ "tag": "
",
+ }
+
+ err := (AsciiJSON{data1}).Render(w1)
+
+ assert.NoError(t, err)
+ assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
+ assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
+
+ w2 := httptest.NewRecorder()
+ data2 := float64(3.1415926)
+
+ err = (AsciiJSON{data2}).Render(w2)
+ assert.NoError(t, err)
+ assert.Equal(t, "3.1415926", w2.Body.String())
+}
+
+func TestRenderAsciiJSONFail(t *testing.T) {
+ w := httptest.NewRecorder()
+ data := make(chan int)
+
+ // json: unsupported type: chan int
+ assert.Error(t, (AsciiJSON{data}).Render(w))
+}
+
type xmlmap map[string]interface{}
// Allows type H to be used with xml.Marshal
@@ -221,6 +269,35 @@ func TestRenderYAMLFail(t *testing.T) {
assert.Error(t, err)
}
+// test Protobuf rendering
+func TestRenderProtoBuf(t *testing.T) {
+ w := httptest.NewRecorder()
+ reps := []int64{int64(1), int64(2)}
+ label := "test"
+ data := &testdata.Test{
+ Label: &label,
+ Reps: reps,
+ }
+
+ (ProtoBuf{data}).WriteContentType(w)
+ protoData, err := proto.Marshal(data)
+ assert.NoError(t, err)
+ assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
+
+ err = (ProtoBuf{data}).Render(w)
+
+ assert.NoError(t, err)
+ assert.Equal(t, string(protoData), w.Body.String())
+ assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
+}
+
+func TestRenderProtoBufFail(t *testing.T) {
+ w := httptest.NewRecorder()
+ data := &testdata.Test{}
+ err := (ProtoBuf{data}).Render(w)
+ assert.Error(t, err)
+}
+
func TestRenderXML(t *testing.T) {
w := httptest.NewRecorder()
data := xmlmap{
@@ -242,7 +319,7 @@ func TestRenderRedirect(t *testing.T) {
assert.NoError(t, err)
data1 := Redirect{
- Code: 301,
+ Code: http.StatusMovedPermanently,
Request: req,
Location: "/new/location",
}
@@ -252,7 +329,7 @@ func TestRenderRedirect(t *testing.T) {
assert.NoError(t, err)
data2 := Redirect{
- Code: 200,
+ Code: http.StatusOK,
Request: req,
Location: "/new/location",
}
@@ -344,7 +421,7 @@ func TestRenderHTMLTemplateEmptyName(t *testing.T) {
func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder()
- htmlRender := HTMLDebug{Files: []string{"../fixtures/basic/hello.tmpl"},
+ htmlRender := HTMLDebug{Files: []string{"../testdata/template/hello.tmpl"},
Glob: "",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
@@ -363,7 +440,7 @@ func TestRenderHTMLDebugFiles(t *testing.T) {
func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{Files: nil,
- Glob: "../fixtures/basic/hello*",
+ Glob: "../testdata/template/hello*",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
@@ -403,7 +480,7 @@ func TestRenderReader(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, body, w.Body.String())
- assert.Equal(t, "image/png", w.HeaderMap.Get("Content-Type"))
- assert.Equal(t, strconv.Itoa(len(body)), w.HeaderMap.Get("Content-Length"))
- assert.Equal(t, headers["Content-Disposition"], w.HeaderMap.Get("Content-Disposition"))
+ assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
+ assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
+ assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
}
diff --git a/render/text.go b/render/text.go
index 74cd26beef..2ea7343cb2 100644
--- a/render/text.go
+++ b/render/text.go
@@ -10,6 +10,7 @@ import (
"net/http"
)
+// String contains the given interface object slice and its format.
type String struct {
Format string
Data []interface{}
@@ -17,20 +18,23 @@ type String struct {
var plainContentType = []string{"text/plain; charset=utf-8"}
+// Render (String) writes data with custom ContentType.
func (r String) Render(w http.ResponseWriter) error {
WriteString(w, r.Format, r.Data)
return nil
}
+// WriteContentType (String) writes Plain ContentType.
func (r String) WriteContentType(w http.ResponseWriter) {
writeContentType(w, plainContentType)
}
+// WriteString writes data according to its format and write custom ContentType.
func WriteString(w http.ResponseWriter, format string, data []interface{}) {
writeContentType(w, plainContentType)
if len(data) > 0 {
fmt.Fprintf(w, format, data...)
- } else {
- io.WriteString(w, format)
+ return
}
+ io.WriteString(w, format)
}
diff --git a/render/xml.go b/render/xml.go
index cff1ac3e81..cc5390a2d0 100644
--- a/render/xml.go
+++ b/render/xml.go
@@ -9,17 +9,20 @@ import (
"net/http"
)
+// XML contains the given interface object.
type XML struct {
Data interface{}
}
var xmlContentType = []string{"application/xml; charset=utf-8"}
+// Render (XML) encodes the given interface object and writes data with custom ContentType.
func (r XML) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
return xml.NewEncoder(w).Encode(r.Data)
}
+// WriteContentType (XML) writes XML ContentType for response.
func (r XML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, xmlContentType)
}
diff --git a/render/yaml.go b/render/yaml.go
index 25d0ebd474..33bc32545f 100644
--- a/render/yaml.go
+++ b/render/yaml.go
@@ -10,12 +10,14 @@ import (
"gopkg.in/yaml.v2"
)
+// YAML contains the given interface object.
type YAML struct {
Data interface{}
}
var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
+// Render (YAML) marshals the given interface object and writes data with custom ContentType.
func (r YAML) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
@@ -28,6 +30,7 @@ func (r YAML) Render(w http.ResponseWriter) error {
return nil
}
+// WriteContentType (YAML) writes YAML ContentType for response.
func (r YAML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, yamlContentType)
}
diff --git a/response_writer.go b/response_writer.go
index 232f00aacc..923b53f8b8 100644
--- a/response_writer.go
+++ b/response_writer.go
@@ -13,10 +13,10 @@ import (
const (
noWritten = -1
- defaultStatus = 200
+ defaultStatus = http.StatusOK
)
-type ResponseWriter interface {
+type responseWriterBase interface {
http.ResponseWriter
http.Hijacker
http.Flusher
@@ -110,5 +110,6 @@ func (w *responseWriter) CloseNotify() <-chan bool {
// Flush implements the http.Flush interface.
func (w *responseWriter) Flush() {
+ w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush()
}
diff --git a/response_writer_1.7.go b/response_writer_1.7.go
new file mode 100644
index 0000000000..801d196b09
--- /dev/null
+++ b/response_writer_1.7.go
@@ -0,0 +1,12 @@
+// +build !go1.8
+
+// Copyright 2018 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+// ResponseWriter ...
+type ResponseWriter interface {
+ responseWriterBase
+}
diff --git a/response_writer_1.8.go b/response_writer_1.8.go
new file mode 100644
index 0000000000..527c00383a
--- /dev/null
+++ b/response_writer_1.8.go
@@ -0,0 +1,25 @@
+// +build go1.8
+
+// Copyright 2018 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+ "net/http"
+)
+
+// ResponseWriter ...
+type ResponseWriter interface {
+ responseWriterBase
+ // get the http.Pusher for server push
+ Pusher() http.Pusher
+}
+
+func (w *responseWriter) Pusher() (pusher http.Pusher) {
+ if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
+ return pusher
+ }
+ return nil
+}
diff --git a/response_writer_test.go b/response_writer_test.go
index cec27338d1..cc5a89dcf9 100644
--- a/response_writer_test.go
+++ b/response_writer_test.go
@@ -35,10 +35,10 @@ func TestResponseWriterReset(t *testing.T) {
writer.reset(testWritter)
assert.Equal(t, -1, writer.size)
- assert.Equal(t, 200, writer.status)
+ assert.Equal(t, http.StatusOK, writer.status)
assert.Equal(t, testWritter, writer.ResponseWriter)
assert.Equal(t, -1, w.Size())
- assert.Equal(t, 200, w.Status())
+ assert.Equal(t, http.StatusOK, w.Status())
assert.False(t, w.Written())
}
@@ -48,13 +48,13 @@ func TestResponseWriterWriteHeader(t *testing.T) {
writer.reset(testWritter)
w := ResponseWriter(writer)
- w.WriteHeader(300)
+ w.WriteHeader(http.StatusMultipleChoices)
assert.False(t, w.Written())
- assert.Equal(t, 300, w.Status())
- assert.NotEqual(t, testWritter.Code, 300)
+ assert.Equal(t, http.StatusMultipleChoices, w.Status())
+ assert.NotEqual(t, http.StatusMultipleChoices, testWritter.Code)
w.WriteHeader(-1)
- assert.Equal(t, 300, w.Status())
+ assert.Equal(t, http.StatusMultipleChoices, w.Status())
}
func TestResponseWriterWriteHeadersNow(t *testing.T) {
@@ -63,12 +63,12 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
writer.reset(testWritter)
w := ResponseWriter(writer)
- w.WriteHeader(300)
+ w.WriteHeader(http.StatusMultipleChoices)
w.WriteHeaderNow()
assert.True(t, w.Written())
assert.Equal(t, 0, w.Size())
- assert.Equal(t, 300, testWritter.Code)
+ assert.Equal(t, http.StatusMultipleChoices, testWritter.Code)
writer.size = 10
w.WriteHeaderNow()
@@ -84,8 +84,8 @@ func TestResponseWriterWrite(t *testing.T) {
n, err := w.Write([]byte("hola"))
assert.Equal(t, 4, n)
assert.Equal(t, 4, w.Size())
- assert.Equal(t, 200, w.Status())
- assert.Equal(t, 200, testWritter.Code)
+ assert.Equal(t, http.StatusOK, w.Status())
+ assert.Equal(t, http.StatusOK, testWritter.Code)
assert.Equal(t, "hola", testWritter.Body.String())
assert.NoError(t, err)
@@ -113,3 +113,19 @@ func TestResponseWriterHijack(t *testing.T) {
w.Flush()
}
+
+func TestResponseWriterFlush(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ writer := &responseWriter{}
+ writer.reset(w)
+
+ writer.WriteHeader(http.StatusInternalServerError)
+ writer.Flush()
+ }))
+ defer testServer.Close()
+
+ // should return 500
+ resp, err := http.Get(testServer.URL)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
+}
diff --git a/routergroup.go b/routergroup.go
index 5e681c1cbd..579aa7dc93 100644
--- a/routergroup.go
+++ b/routergroup.go
@@ -11,11 +11,13 @@ import (
"strings"
)
+// IRouter defines all router handle interface includes single and group router.
type IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}
+// IRoutes defines all router handle interface.
type IRoutes interface {
Use(...HandlerFunc) IRoutes
@@ -34,8 +36,8 @@ type IRoutes interface {
StaticFS(string, http.FileSystem) IRoutes
}
-// RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix
-// and an array of handlers (middleware).
+// RouterGroup is used internally to configure router, a RouterGroup is associated with
+// a prefix and an array of handlers (middleware).
type RouterGroup struct {
Handlers HandlersChain
basePath string
@@ -61,6 +63,8 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R
}
}
+// BasePath returns the base path of router group.
+// For example, if v := router.Group("/rest/n/v1/api"), v.BasePath() is "/rest/n/v1/api".
func (group *RouterGroup) BasePath() string {
return group.basePath
}
@@ -139,7 +143,7 @@ func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRou
return group.returnObj()
}
-// StaticFile registers a single route in order to server a single file of the local filesystem.
+// StaticFile registers a single route in order to serve a single file of the local filesystem.
// router.StaticFile("favicon.ico", "./resources/favicon.ico")
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
@@ -184,7 +188,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
_, nolisting := fs.(*onlyfilesFS)
return func(c *Context) {
if nolisting {
- c.Writer.WriteHeader(404)
+ c.Writer.WriteHeader(http.StatusNotFound)
}
fileServer.ServeHTTP(c.Writer, c.Request)
}
diff --git a/routergroup_test.go b/routergroup_test.go
index a362e23dd6..ce3d54a2f6 100644
--- a/routergroup_test.go
+++ b/routergroup_test.go
@@ -5,6 +5,7 @@
package gin
import (
+ "net/http"
"testing"
"github.com/stretchr/testify/assert"
@@ -50,7 +51,7 @@ func performRequestInGroup(t *testing.T, method string) {
assert.Equal(t, "/v1/login/", login.BasePath())
handler := func(c *Context) {
- c.String(400, "the method was %s and index %d", c.Request.Method, c.index)
+ c.String(http.StatusBadRequest, "the method was %s and index %d", c.Request.Method, c.index)
}
switch method {
@@ -80,11 +81,11 @@ func performRequestInGroup(t *testing.T, method string) {
}
w := performRequest(router, method, "/v1/login/test")
- assert.Equal(t, 400, w.Code)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "the method was "+method+" and index 3", w.Body.String())
w = performRequest(router, method, "/v1/test")
- assert.Equal(t, 400, w.Code)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "the method was "+method+" and index 1", w.Body.String())
}
diff --git a/routes_test.go b/routes_test.go
index 8129390711..60f1c81b6f 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -80,20 +80,20 @@ func testRouteNotOK2(method string, t *testing.T) {
func TestRouterMethod(t *testing.T) {
router := New()
router.PUT("/hey2", func(c *Context) {
- c.String(200, "sup2")
+ c.String(http.StatusOK, "sup2")
})
router.PUT("/hey", func(c *Context) {
- c.String(200, "called")
+ c.String(http.StatusOK, "called")
})
router.PUT("/hey3", func(c *Context) {
- c.String(200, "sup3")
+ c.String(http.StatusOK, "sup3")
})
w := performRequest(router, "PUT", "/hey")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "called", w.Body.String())
}
@@ -144,42 +144,42 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
w := performRequest(router, "GET", "/path/")
assert.Equal(t, "/path", w.Header().Get("Location"))
- assert.Equal(t, 301, w.Code)
+ assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "GET", "/path2")
assert.Equal(t, "/path2/", w.Header().Get("Location"))
- assert.Equal(t, 301, w.Code)
+ assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "POST", "/path3/")
assert.Equal(t, "/path3", w.Header().Get("Location"))
- assert.Equal(t, 307, w.Code)
+ assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "PUT", "/path4")
assert.Equal(t, "/path4/", w.Header().Get("Location"))
- assert.Equal(t, 307, w.Code)
+ assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "GET", "/path")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "GET", "/path2/")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "POST", "/path3")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "PUT", "/path4/")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
router.RedirectTrailingSlash = false
w = performRequest(router, "GET", "/path/")
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "GET", "/path2")
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "POST", "/path3/")
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "PUT", "/path4")
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestRouteRedirectFixedPath(t *testing.T) {
@@ -194,19 +194,19 @@ func TestRouteRedirectFixedPath(t *testing.T) {
w := performRequest(router, "GET", "/PATH")
assert.Equal(t, "/path", w.Header().Get("Location"))
- assert.Equal(t, 301, w.Code)
+ assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "GET", "/path2")
assert.Equal(t, "/Path2", w.Header().Get("Location"))
- assert.Equal(t, 301, w.Code)
+ assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "POST", "/path3")
assert.Equal(t, "/PATH3", w.Header().Get("Location"))
- assert.Equal(t, 307, w.Code)
+ assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "POST", "/path4")
assert.Equal(t, "/Path4/", w.Header().Get("Location"))
- assert.Equal(t, 307, w.Code)
+ assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
}
// TestContextParamsGet tests that a parameter can be parsed from the URL.
@@ -236,7 +236,7 @@ func TestRouteParamsByName(t *testing.T) {
w := performRequest(router, "GET", "/test/john/smith/is/super/great")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "john", name)
assert.Equal(t, "smith", lastName)
assert.Equal(t, "/is/super/great", wild)
@@ -265,15 +265,15 @@ func TestRouteStaticFile(t *testing.T) {
w2 := performRequest(router, "GET", "/result")
assert.Equal(t, w, w2)
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "Gin Web Framework", w.Body.String())
- assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+ assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
w3 := performRequest(router, "HEAD", "/using_static/"+filename)
w4 := performRequest(router, "HEAD", "/result")
assert.Equal(t, w3, w4)
- assert.Equal(t, 200, w3.Code)
+ assert.Equal(t, http.StatusOK, w3.Code)
}
// TestHandleStaticDir - ensure the root/sub dir handles properly
@@ -283,9 +283,9 @@ func TestRouteStaticListingDir(t *testing.T) {
w := performRequest(router, "GET", "/")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "gin.go")
- assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+ assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestHandleHeadToDir - ensure the root/sub dir handles properly
@@ -295,7 +295,7 @@ func TestRouteStaticNoListing(t *testing.T) {
w := performRequest(router, "GET", "/")
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
assert.NotContains(t, w.Body.String(), "gin.go")
}
@@ -310,12 +310,12 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
w := performRequest(router, "GET", "/gin.go")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin")
- assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type"))
- assert.NotEqual(t, w.HeaderMap.Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
- assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.HeaderMap.Get("Expires"))
- assert.Equal(t, "Gin Framework", w.HeaderMap.Get("x-GIN"))
+ assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
+ assert.NotEqual(t, w.Header().Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
+ assert.Equal(t, "Mon, 02 Jan 2006 15:04:05 MST", w.Header().Get("Expires"))
+ assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
}
func TestRouteNotAllowedEnabled(t *testing.T) {
@@ -333,19 +333,29 @@ func TestRouteNotAllowedEnabled(t *testing.T) {
assert.Equal(t, http.StatusTeapot, w.Code)
}
+func TestRouteNotAllowedEnabled2(t *testing.T) {
+ router := New()
+ router.HandleMethodNotAllowed = true
+ // add one methodTree to trees
+ router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}})
+ router.GET("/path2", func(c *Context) {})
+ w := performRequest(router, "POST", "/path2")
+ assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
+}
+
func TestRouteNotAllowedDisabled(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = false
router.POST("/path", func(c *Context) {})
w := performRequest(router, "GET", "/path")
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText")
})
w = performRequest(router, "GET", "/path")
assert.Equal(t, "404 page not found", w.Body.String())
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestRouterNotFound(t *testing.T) {
@@ -360,20 +370,20 @@ func TestRouterNotFound(t *testing.T) {
code int
location string
}{
- {"/path/", 301, "/path"}, // TSR -/
- {"/dir", 301, "/dir/"}, // TSR +/
- {"", 301, "/"}, // TSR +/
- {"/PATH", 301, "/path"}, // Fixed Case
- {"/DIR/", 301, "/dir/"}, // Fixed Case
- {"/PATH/", 301, "/path"}, // Fixed Case -/
- {"/DIR", 301, "/dir/"}, // Fixed Case +/
- {"/../path", 301, "/path"}, // CleanPath
- {"/nope", 404, ""}, // NotFound
+ {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
+ {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
+ {"", http.StatusMovedPermanently, "/"}, // TSR +/
+ {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
+ {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
+ {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
+ {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
+ {"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath
+ {"/nope", http.StatusNotFound, ""}, // NotFound
}
for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route)
assert.Equal(t, tr.code, w.Code)
- if w.Code != 404 {
+ if w.Code != http.StatusNotFound {
assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
}
}
@@ -381,24 +391,24 @@ func TestRouterNotFound(t *testing.T) {
// Test custom not found handler
var notFound bool
router.NoRoute(func(c *Context) {
- c.AbortWithStatus(404)
+ c.AbortWithStatus(http.StatusNotFound)
notFound = true
})
w := performRequest(router, "GET", "/nope")
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
assert.True(t, notFound)
// Test other method than GET (want 307 instead of 301)
router.PATCH("/path", func(c *Context) {})
w = performRequest(router, "PATCH", "/path/")
- assert.Equal(t, 307, w.Code)
+ assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, "map[Location:[/path]]", fmt.Sprint(w.Header()))
// Test special case where no node for the prefix "/" exists
router = New()
router.GET("/a", func(c *Context) {})
w = performRequest(router, "GET", "/")
- assert.Equal(t, 404, w.Code)
+ assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestRouteRawPath(t *testing.T) {
@@ -417,7 +427,7 @@ func TestRouteRawPath(t *testing.T) {
})
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
}
func TestRouteRawPathNoUnescape(t *testing.T) {
@@ -437,7 +447,7 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
})
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333")
- assert.Equal(t, 200, w.Code)
+ assert.Equal(t, http.StatusOK, w.Code)
}
func TestRouteServeErrorWithWriteHeader(t *testing.T) {
diff --git a/test_helpers.go b/test_helpers.go
index 2aed37f206..3a7a5ddf69 100644
--- a/test_helpers.go
+++ b/test_helpers.go
@@ -4,9 +4,7 @@
package gin
-import (
- "net/http"
-)
+import "net/http"
// CreateTestContext returns a fresh engine and context for testing purposes
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
diff --git a/fixtures/testdata/cert.pem b/testdata/certificate/cert.pem
similarity index 100%
rename from fixtures/testdata/cert.pem
rename to testdata/certificate/cert.pem
diff --git a/fixtures/testdata/key.pem b/testdata/certificate/key.pem
similarity index 100%
rename from fixtures/testdata/key.pem
rename to testdata/certificate/key.pem
diff --git a/binding/example/test.pb.go b/testdata/protoexample/test.pb.go
similarity index 94%
rename from binding/example/test.pb.go
rename to testdata/protoexample/test.pb.go
index 3de8444ffb..21997ca1c6 100644
--- a/binding/example/test.pb.go
+++ b/testdata/protoexample/test.pb.go
@@ -3,7 +3,7 @@
// DO NOT EDIT!
/*
-Package example is a generated protocol buffer package.
+Package protoexample is a generated protocol buffer package.
It is generated from these files:
test.proto
@@ -11,7 +11,7 @@ It is generated from these files:
It has these top-level messages:
Test
*/
-package example
+package protoexample
import proto "github.com/golang/protobuf/proto"
import math "math"
@@ -109,5 +109,5 @@ func (m *Test_OptionalGroup) GetRequiredField() string {
}
func init() {
- proto.RegisterEnum("example.FOO", FOO_name, FOO_value)
+ proto.RegisterEnum("protoexample.FOO", FOO_name, FOO_value)
}
diff --git a/binding/example/test.proto b/testdata/protoexample/test.proto
similarity index 90%
rename from binding/example/test.proto
rename to testdata/protoexample/test.proto
index 8ee9800aa6..3e73428775 100644
--- a/binding/example/test.proto
+++ b/testdata/protoexample/test.proto
@@ -1,4 +1,4 @@
-package example;
+package protoexample;
enum FOO {X=17;};
diff --git a/fixtures/basic/hello.tmpl b/testdata/template/hello.tmpl
similarity index 100%
rename from fixtures/basic/hello.tmpl
rename to testdata/template/hello.tmpl
diff --git a/fixtures/basic/raw.tmpl b/testdata/template/raw.tmpl
similarity index 100%
rename from fixtures/basic/raw.tmpl
rename to testdata/template/raw.tmpl
diff --git a/tools.go b/tools.go
new file mode 100644
index 0000000000..9f96406ac2
--- /dev/null
+++ b/tools.go
@@ -0,0 +1,25 @@
+// 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 tools
+
+// This file exists to cause `go mod` and `go get` to believe these tools
+// are dependencies, even though they are not runtime dependencies of any
+// gin package. This means they will appear in `go.mod` file, but will not
+// be a part of the build.
+
+package gin
+
+import (
+ _ "github.com/campoy/embedmd"
+ _ "github.com/client9/misspell/cmd/misspell"
+ _ "github.com/dustin/go-broadcast"
+ _ "github.com/gin-gonic/autotls"
+ _ "github.com/jessevdk/go-assets"
+ _ "github.com/manucorporat/stats"
+ _ "github.com/thinkerou/favicon"
+ _ "golang.org/x/crypto/acme/autocert"
+ _ "golang.org/x/lint/golint"
+ _ "google.golang.org/grpc"
+)
diff --git a/tree.go b/tree.go
index b653066521..ada62ceb5e 100644
--- a/tree.go
+++ b/tree.go
@@ -193,9 +193,16 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
}
}
- panic("path segment '" + path +
+ pathSeg := path
+ if n.nType != catchAll {
+ pathSeg = strings.SplitN(path, "/", 2)[0]
+ }
+ prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
+ panic("'" + pathSeg +
+ "' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
- "' in path '" + fullPath + "'")
+ "' in existing prefix '" + prefix +
+ "'")
}
c := path[0]
diff --git a/tree_test.go b/tree_test.go
index 5bc271712b..a1b3bbe781 100644
--- a/tree_test.go
+++ b/tree_test.go
@@ -5,7 +5,9 @@
package gin
import (
+ "fmt"
"reflect"
+ "regexp"
"strings"
"testing"
)
@@ -125,8 +127,6 @@ func TestTreeAddAndGet(t *testing.T) {
tree.addRoute(route, fakeHandler(route))
}
- //printChildren(tree, "")
-
checkRequests(t, tree, testRequests{
{"/a", false, "/a", nil},
{"/", true, "", nil},
@@ -168,8 +168,6 @@ func TestTreeWildcard(t *testing.T) {
tree.addRoute(route, fakeHandler(route))
}
- //printChildren(tree, "")
-
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
@@ -208,7 +206,6 @@ func TestUnescapeParameters(t *testing.T) {
tree.addRoute(route, fakeHandler(route))
}
- //printChildren(tree, "")
unescape := true
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
@@ -260,8 +257,6 @@ func testRoutes(t *testing.T, routes []testRoute) {
t.Errorf("unexpected panic for route '%s': %v", route.path, recv)
}
}
-
- //printChildren(tree, "")
}
func TestTreeWildcardConflict(t *testing.T) {
@@ -328,8 +323,6 @@ func TestTreeDupliatePath(t *testing.T) {
}
}
- //printChildren(tree, "")
-
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
{"/doc/", false, "/doc/", nil},
@@ -444,8 +437,6 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
}
}
- //printChildren(tree, "")
-
tsrRoutes := [...]string{
"/hi/",
"/b",
@@ -664,3 +655,43 @@ func TestTreeInvalidNodeType(t *testing.T) {
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
}
}
+
+func TestTreeWildcardConflictEx(t *testing.T) {
+ conflicts := [...]struct {
+ route string
+ segPath string
+ existPath string
+ existSegPath string
+ }{
+ {"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`},
+ {"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`},
+ {"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`},
+ {"/conxxx", "xxx", `/con:tact`, `:tact`},
+ {"/conooo/xxx", "ooo", `/con:tact`, `:tact`},
+ }
+
+ for _, conflict := range conflicts {
+ // I have to re-create a 'tree', because the 'tree' will be
+ // in an inconsistent state when the loop recovers from the
+ // panic which threw by 'addRoute' function.
+ tree := &node{}
+ routes := [...]string{
+ "/con:tact",
+ "/who/are/*you",
+ "/who/foo/hello",
+ }
+
+ for _, route := range routes {
+ tree.addRoute(route, fakeHandler(route))
+ }
+
+ recv := catchPanic(func() {
+ tree.addRoute(conflict.route, fakeHandler(conflict.route))
+ })
+
+ if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'",
+ conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) {
+ t.Fatalf("invalid wildcard conflict error (%v)", recv)
+ }
+ }
+}
diff --git a/utils.go b/utils.go
index 278029d7c2..f4532d56cd 100644
--- a/utils.go
+++ b/utils.go
@@ -14,8 +14,10 @@ import (
"strings"
)
+// BindKey indicates a default bind key.
const BindKey = "_gin-gonic/gin/bindkey"
+// Bind is a helper function for given interface object and returns a Gin middleware.
func Bind(val interface{}) HandlerFunc {
value := reflect.ValueOf(val)
if value.Kind() == reflect.Ptr {
@@ -33,16 +35,14 @@ func Bind(val interface{}) HandlerFunc {
}
}
-// WrapF is a helper function for wrapping http.HandlerFunc
-// Returns a Gin middleware
+// WrapF is a helper function for wrapping http.HandlerFunc and returns a Gin middleware.
func WrapF(f http.HandlerFunc) HandlerFunc {
return func(c *Context) {
f(c.Writer, c.Request)
}
}
-// WrapH is a helper function for wrapping http.Handler
-// Returns a Gin middleware
+// WrapH is a helper function for wrapping http.Handler and returns a Gin middleware.
func WrapH(h http.Handler) HandlerFunc {
return func(c *Context) {
h.ServeHTTP(c.Writer, c.Request)
@@ -103,10 +103,7 @@ func parseAccept(acceptHeader string) []string {
parts := strings.Split(acceptHeader, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
- if index := strings.IndexByte(part, ';'); index >= 0 {
- part = part[0:index]
- }
- if part = strings.TrimSpace(part); part != "" {
+ if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" {
out = append(out, part)
}
}
diff --git a/utils_test.go b/utils_test.go
index 3d019e7e25..9b57c57b0a 100644
--- a/utils_test.go
+++ b/utils_test.go
@@ -5,6 +5,8 @@
package gin
import (
+ "bytes"
+ "encoding/xml"
"fmt"
"net/http"
"testing"
@@ -23,7 +25,7 @@ type testStruct struct {
func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) {
assert.Equal(t.T, "POST", req.Method)
assert.Equal(t.T, "/path", req.URL.Path)
- w.WriteHeader(500)
+ w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "hello")
}
@@ -33,16 +35,16 @@ func TestWrap(t *testing.T) {
router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) {
assert.Equal(t, "GET", req.Method)
assert.Equal(t, "/path2", req.URL.Path)
- w.WriteHeader(400)
+ w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "hola!")
}))
w := performRequest(router, "POST", "/path")
- assert.Equal(t, 500, w.Code)
+ assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "hello", w.Body.String())
w = performRequest(router, "GET", "/path2")
- assert.Equal(t, 400, w.Code)
+ assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "hola!", w.Body.String())
}
@@ -124,3 +126,14 @@ func TestBindMiddleware(t *testing.T) {
Bind(&bindTestStruct{})
})
}
+
+func TestMarshalXMLforH(t *testing.T) {
+ h := H{
+ "": "test",
+ }
+ var b bytes.Buffer
+ enc := xml.NewEncoder(&b)
+ var x xml.StartElement
+ e := h.MarshalXML(enc, x)
+ assert.Error(t, e)
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 5ace49df56..af1a0148af 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -1,19 +1,14 @@
{
- "comment": "v1.2",
+ "comment": "v1.3.0",
"ignore": "test",
"package": [
{
- "checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=",
- "comment": "v1.1.0",
+ "checksumSHA1": "CSPbwbyzqA6sfORicn4HFtIhF/c=",
"path": "github.com/davecgh/go-spew/spew",
- "revision": "346938d642f2ec3594ed81d874461961cd0faa76",
- "revisionTime": "2016-10-29T20:57:26Z"
- },
- {
- "checksumSHA1": "7c3FuEadBInl/4ExSrB7iJMXpe4=",
- "path": "github.com/dustin/go-broadcast",
- "revision": "3bdf6d4a7164a50bc19d5f230e2981d87d2584f1",
- "revisionTime": "2014-06-27T04:00:55Z"
+ "revision": "8991bc29aa16c548c550c7ff78260e27b9ab7c73",
+ "revisionTime": "2018-02-21T22:46:20Z",
+ "version": "v1.1",
+ "versionExact": "v1.1.1"
},
{
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
@@ -22,111 +17,72 @@
"revisionTime": "2017-01-09T09:34:21Z"
},
{
- "checksumSHA1": "+vZNyF2MykVjenLg1TpjjgjthV0=",
- "path": "github.com/gin-gonic/autotls",
- "revision": "8ca25fbde72bb72a00466215b94b489c71fcb815",
- "revisionTime": "2017-09-16T16:54:15Z"
- },
- {
- "checksumSHA1": "qlPUeFabwF4RKAOF1H+yBFU1Veg=",
+ "checksumSHA1": "mE9XW26JSpe4meBObM6J/Oeq0eg=",
"path": "github.com/golang/protobuf/proto",
- "revision": "5a0f697c9ed9d68fef0116532c6e05cfeae00e55",
- "revisionTime": "2017-06-01T23:02:30Z"
+ "revision": "aa810b61a9c79d51363740d207bb46cf8e620ed5",
+ "revisionTime": "2018-08-14T21:14:27Z",
+ "version": "v1.2",
+ "versionExact": "v1.2.0"
},
{
- "checksumSHA1": "Cq9h7eDNXXyR/qJPvO8/Rk4pmFg=",
- "path": "github.com/jessevdk/go-assets",
- "revision": "4f4301a06e153ff90e17793577ab6bf79f8dc5c5",
- "revisionTime": "2016-09-21T14:41:39Z"
- },
- {
- "checksumSHA1": "Ajh8TemnItg4nn+jKmVcsMRALBc=",
+ "checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=",
"path": "github.com/json-iterator/go",
- "revision": "36b14963da70d11297d313183d7e6388c8510e1e",
- "revisionTime": "2017-08-29T15:58:51Z"
- },
- {
- "checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=",
- "path": "github.com/manucorporat/stats",
- "revision": "8f2d6ace262eba462e9beb552382c98be51d807b",
- "revisionTime": "2015-05-31T20:46:25Z"
+ "revision": "1624edc4454b8682399def8740d46db5e4362ba4",
+ "revisionTime": "2018-08-06T06:07:27Z",
+ "version": "v1.1",
+ "versionExact": "v1.1.5"
},
{
- "checksumSHA1": "U6lX43KDDlNOn+Z0Yyww+ZzHfFo=",
+ "checksumSHA1": "w5RcOnfv5YDr3j2bd1YydkPiZx4=",
"path": "github.com/mattn/go-isatty",
- "revision": "57fdcb988a5c543893cc61bce354a6e24ab70022",
- "revisionTime": "2017-03-07T16:30:44Z"
- },
- {
- "checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
- "comment": "v1.0.0",
- "path": "github.com/pmezard/go-difflib/difflib",
- "revision": "792786c7400a136282c1664665ae0a8db921c6c2",
- "revisionTime": "2016-01-10T10:55:54Z"
+ "revision": "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c",
+ "revisionTime": "2017-11-07T05:05:31Z",
+ "version": "v0.0",
+ "versionExact": "v0.0.4"
},
{
- "checksumSHA1": "Q2V7Zs3diLmLfmfbiuLpSxETSuY=",
- "comment": "v1.1.4",
+ "checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=",
"path": "github.com/stretchr/testify/assert",
- "revision": "976c720a22c8eb4eb6a0b4348ad85ad12491a506",
- "revisionTime": "2016-09-25T22:06:09Z"
- },
- {
- "checksumSHA1": "IopMW+arBezL5bqOfrVU6UEfn28=",
- "path": "github.com/thinkerou/favicon",
- "revision": "94a442a49da6e2d44bdd5e0d2e2e185c43a19d93",
- "revisionTime": "2017-07-10T14:05:20Z"
+ "revision": "f35b8ab0b5a2cef36673838d662e249dd9c94686",
+ "revisionTime": "2018-05-06T18:05:49Z",
+ "version": "v1.2",
+ "versionExact": "v1.2.2"
},
{
- "checksumSHA1": "CoxdaTYdPZNJXr8mJfLxye428N0=",
+ "checksumSHA1": "5Bd8RPhhaKcEXkagzPqymP4Gx5E=",
"path": "github.com/ugorji/go/codec",
- "revision": "c88ee250d0221a57af388746f5cf03768c21d6e2",
- "revisionTime": "2017-02-15T20:11:44Z"
- },
- {
- "checksumSHA1": "W0j4I7QpxXlChjyhAojZmFjU6Bg=",
- "path": "golang.org/x/crypto/acme",
- "revision": "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d",
- "revisionTime": "2017-06-19T06:03:41Z"
+ "revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab",
+ "revisionTime": "2018-04-07T10:07:33Z",
+ "version": "v1.1",
+ "versionExact": "v1.1.1"
},
{
- "checksumSHA1": "TrKJW+flz7JulXU3sqnBJjGzgQc=",
- "path": "golang.org/x/crypto/acme/autocert",
- "revision": "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d",
- "revisionTime": "2017-06-19T06:03:41Z"
- },
- {
- "checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
- "comment": "release-branch.go1.7",
+ "checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
"path": "golang.org/x/net/context",
- "revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a",
- "revisionTime": "2016-10-18T08:54:36Z"
- },
- {
- "checksumSHA1": "S0DP7Pn7sZUmXc55IzZnNvERu6s=",
- "path": "golang.org/x/sync/errgroup",
- "revision": "8e0aa688b654ef28caa72506fa5ec8dba9fc7690",
- "revisionTime": "2017-07-19T03:38:01Z"
+ "revision": "49bb7cea24b1df9410e1712aa6433dae904ff66a",
+ "revisionTime": "2018-10-11T05:27:23Z"
},
{
- "checksumSHA1": "TVEkpH3gq84iQ39I4R+mlDwjuVI=",
+ "checksumSHA1": "SiJNkx+YGtq3Gtr6Ldu6OW83O+U=",
"path": "golang.org/x/sys/unix",
- "revision": "99f16d856c9836c42d24e7ab64ea72916925fa97",
- "revisionTime": "2017-03-08T15:04:45Z"
+ "revision": "fa43e7bc11baaae89f3f902b2b4d832b68234844",
+ "revisionTime": "2018-10-11T14:35:51Z"
},
{
- "checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=",
- "comment": "v8.18.1",
+ "checksumSHA1": "P/k5ZGf0lEBgpKgkwy++F7K1PSg=",
"path": "gopkg.in/go-playground/validator.v8",
- "revision": "5f57d2222ad794d0dffb07e664ea05e2ee07d60c",
- "revisionTime": "2016-07-18T13:41:25Z"
+ "revision": "5f1438d3fca68893a817e4a66806cea46a9e4ebf",
+ "revisionTime": "2017-07-30T05:02:35Z",
+ "version": "v8.18.2",
+ "versionExact": "v8.18.2"
},
{
- "checksumSHA1": "12GqsW8PiRPnezDDy0v4brZrndM=",
- "comment": "v2",
+ "checksumSHA1": "ZSWoOPUNRr5+3dhkLK3C4cZAQPk=",
"path": "gopkg.in/yaml.v2",
- "revision": "a5b47d31c556af34a302ce5d659e6fea44d90de0",
- "revisionTime": "2016-09-28T15:37:09Z"
+ "revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183",
+ "revisionTime": "2018-03-28T19:50:20Z",
+ "version": "v2.2",
+ "versionExact": "v2.2.1"
}
],
"rootPath": "github.com/gin-gonic/gin"
diff --git a/version.go b/version.go
new file mode 100644
index 0000000000..028caebe25
--- /dev/null
+++ b/version.go
@@ -0,0 +1,8 @@
+// Copyright 2018 Gin Core Team. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+// Version is the current gin framework's version.
+const Version = "v1.4.0-dev"