Merge branch 'master' into tree-from-httprouter

This commit is contained in:
田欧 2019-02-20 15:50:48 +08:00 committed by GitHub
commit cecb40bbe9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 3347 additions and 940 deletions

13
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

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

7
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -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.

2
.gitignore vendored
View File

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

View File

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

View File

@ -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.

View File

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

View File

@ -1,7 +1,9 @@
GO ?= go
GOFMT ?= gofmt "-s"
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
VETPACKAGES ?= $(shell go list ./... | grep -v /vendor/ | grep -v /examples/)
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
@ -10,7 +12,22 @@ install: deps
.PHONY: test
test:
sh coverage.sh
echo "mode: count" > coverage.out
for d in $(TESTFOLDER); do \
$(GO) test -v -covermode=count -coverprofile=profile.out $$d > tmp.out; \
cat tmp.out; \
if grep -q "^--- FAIL" tmp.out; then \
rm tmp.out; \
exit 1; \
elif grep -q "build failed" tmp.out; then \
rm tmp.out; \
exit; \
fi; \
if [ -f profile.out ]; then \
cat profile.out | grep -v "mode:" >> coverage.out; \
rm profile.out; \
fi; \
done
.PHONY: fmt
fmt:
@ -18,7 +35,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:"; \
@ -27,14 +43,14 @@ fmt-check:
fi;
vet:
go vet $(VETPACKAGES)
$(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:
@ -43,20 +59,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;

487
README.md
View File

@ -3,11 +3,13 @@
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png">
[![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,18 +29,21 @@ 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)
- [Using middleware](#using-middleware)
- [How to write log file](#how-to-write-log-file)
- [Custom Log Format](#custom-log-format)
- [Model binding and validation](#model-binding-and-validation)
- [Custom Validators](#custom-validators)
- [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind Uri](#bind-uri)
- [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON 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)
@ -55,8 +61,68 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct)
- [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
@ -135,61 +201,9 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894
- [x] Battle tested
- [x] API frozen, new releases will not break your code.
## Start using it
1. Download and install it:
```sh
$ go get github.com/gin-gonic/gin
```
2. Import it in your code:
```go
import "github.com/gin-gonic/gin"
```
3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
```go
import "net/http"
```
### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor)
1. `go get` govendor
```sh
$ go get github.com/kardianos/govendor
```
2. Create your project folder and `cd` inside
```sh
$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
```
3. Vendor init your project and add gin
```sh
$ govendor init
$ govendor fetch github.com/gin-gonic/gin@v1.2
```
4. Copy a starting template inside your project
```sh
$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go
```
5. Run your project
```sh
$ go run main.go
```
## Build with [jsoniter](https://github.com/json-iterator/go)
Gin use `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags.
Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags.
```sh
$ go build -tags=jsoniter .
@ -229,7 +243,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)
@ -316,12 +330,44 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great
```
### Map as querystring or postform parameters
```
POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded
names[first]=thinkerou&names[second]=tianou
```
```go
func main() {
router := gin.Default()
router.POST("/post", func(c *gin.Context) {
ids := c.QueryMap("ids")
names := c.PostFormMap("names")
fmt.Printf("ids: %v; names: %v", ids, names)
})
router.Run(":8080")
}
```
```
ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
```
### Upload files
#### Single file
References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
`file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693)
> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done.
```go
func main() {
router := gin.Default()
@ -487,9 +533,46 @@ func main() {
}
```
### Custom Log Format
```go
func main() {
router := gin.New()
// LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
// By default gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// your custom format
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
router.Use(gin.Recovery())
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
```
**Sample Output**
```
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
```
### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).
Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags).
@ -497,10 +580,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
Also, Gin provides two sets of methods for binding:
- **Type** - Must bind
- **Methods** - `Bind`, `BindJSON`, `BindQuery`
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
- **Type** - Should bind
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindQuery`
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
@ -510,8 +593,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() {
@ -520,30 +603,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 (
// <?xml version="1.0" encoding="UTF-8"?>
// <root>
// <user>user</user>
// <password>123</password>
// </root>)
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
@ -595,6 +703,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"`
@ -642,7 +751,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
@ -688,9 +797,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"`
@ -724,6 +836,40 @@ Test it with:
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"
```
### Bind Uri
See the [detail information](https://github.com/gin-gonic/gin/issues/846).
```go
package main
import "github.com/gin-gonic/gin"
type Person struct {
ID string `uri:"id" binding:"required,uuid"`
Name string `uri:"name" binding:"required"`
}
func main() {
route := gin.Default()
route.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.JSON(400, gin.H{"msg": err})
return
}
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
})
route.Run(":8088")
}
```
Test it with:
```sh
$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
$ curl -v localhost:8088/thinkerou/not-uuid
```
### Bind HTML checkboxes
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
@ -755,12 +901,12 @@ form.html
<form action="/" method="POST">
<p>Check some colors</p>
<label for="red">Red</label>
<input type="checkbox" name="colors[]" value="red" id="red" />
<input type="checkbox" name="colors[]" value="red" id="red">
<label for="green">Green</label>
<input type="checkbox" name="colors[]" value="green" id="green" />
<input type="checkbox" name="colors[]" value="green" id="green">
<label for="blue">Blue</label>
<input type="checkbox" name="colors[]" value="blue" id="blue" />
<input type="submit" />
<input type="checkbox" name="colors[]" value="blue" id="blue">
<input type="submit">
</form>
```
@ -809,7 +955,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() {
@ -843,6 +989,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")
}
@ -893,6 +1052,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": "<br>",
}
// 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": "<b>Hello, world!</b>",
})
})
// Serves literal characters
r.GET("/purejson", func(c *gin.Context) {
c.PureJSON(200, gin.H{
"html": "<b>Hello, world!</b>",
})
})
// listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
```
### Serving static files
```go
@ -1027,7 +1237,7 @@ You may use custom delims
```go
r := gin.Default()
r.Delims("{[{", "}]}")
r.LoadHTMLGlob("/path/to/templates"))
r.LoadHTMLGlob("/path/to/templates")
```
#### Custom Template Funcs
@ -1057,7 +1267,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{}{
@ -1423,6 +1633,7 @@ import (
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
@ -1450,7 +1661,10 @@ func main() {
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
// kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
@ -1459,6 +1673,11 @@ func main() {
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
// catching ctx.Done(). timeout of 5 seconds.
select {
case <-ctx.Done():
log.Println("timeout of 5 seconds.")
}
log.Println("Server exiting")
}
```
@ -1589,11 +1808,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
}
```
@ -1706,6 +1925,78 @@ func main() {
}
```
### Define format for the log of routes
The default log of routes is:
```
[GIN-debug] POST /foo --> main.main.func1 (3 handlers)
[GIN-debug] GET /bar --> main.main.func2 (3 handlers)
[GIN-debug] GET /status --> main.main.func3 (3 handlers)
```
If you want to log this information in given format (e.g. JSON, key values or something else), then you can define this format with `gin.DebugPrintRouteFunc`.
In the example below, we log all routes with standard log package but you can use another log tools that suits of your needs.
```go
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
r.POST("/foo", func(c *gin.Context) {
c.JSON(http.StatusOK, "foo")
})
r.GET("/bar", func(c *gin.Context) {
c.JSON(http.StatusOK, "bar")
})
r.GET("/status", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})
// Listen and Server in http://0.0.0.0:8080
r.Run()
}
```
### Set and get a cookie
```go
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}
```
## Testing
The `net/http/httptest` package is preferable way for HTTP testing.
@ -1752,9 +2043,13 @@ func TestPingRoute(t *testing.T) {
}
```
## Users [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
* [drone](https://github.com/drone/drone): Drone is a Continuous Delivery platform built on Docker, written in Go
* [drone](https://github.com/drone/drone): Drone is a Continuous Delivery platform built on Docker, written in Go.
* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go.
* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform.
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.

View File

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

View File

@ -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"))
}

View File

@ -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")
}

View File

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

View File

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

View File

@ -11,10 +11,11 @@ import (
"io/ioutil"
"mime/multipart"
"net/http"
"strconv"
"testing"
"time"
"github.com/gin-gonic/gin/binding/example"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
@ -190,6 +191,16 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, MsgPack, Default("POST", MIMEMSGPACK))
assert.Equal(t, MsgPack, Default("PUT", MIMEMSGPACK2))
assert.Equal(t, YAML, Default("POST", MIMEYAML))
assert.Equal(t, YAML, Default("PUT", MIMEYAML))
}
func TestBindingJSONNilBody(t *testing.T) {
var obj FooStruct
req, _ := http.NewRequest(http.MethodPost, "/", nil)
err := JSON.Bind(req, &obj)
assert.Error(t, err)
}
func TestBindingJSON(t *testing.T) {
@ -473,6 +484,20 @@ func TestBindingXMLFail(t *testing.T) {
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>")
}
func TestBindingYAML(t *testing.T) {
testBodyBinding(t,
YAML, "yaml",
"/", "/",
`foo: bar`, `bar: foo`)
}
func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t,
YAML, "yaml",
"/", "/",
`foo:\nbar`, `bar: foo`)
}
func createFormPostRequest() *http.Request {
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
req.Header.Set("Content-Type", MIMEPOSTForm)
@ -491,28 +516,28 @@ func createFormPostRequestFail() *http.Request {
return req
}
func createFormMultipartRequest() *http.Request {
func createFormMultipartRequest(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
mw.SetBoundary(boundary)
mw.WriteField("foo", "bar")
mw.WriteField("bar", "foo")
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("foo", "bar"))
assert.NoError(t, mw.WriteField("bar", "foo"))
req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
}
func createFormMultipartRequestFail() *http.Request {
func createFormMultipartRequestFail(t *testing.T) *http.Request {
boundary := "--testboundary"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
defer mw.Close()
mw.SetBoundary(boundary)
mw.WriteField("map_foo", "bar")
assert.NoError(t, mw.SetBoundary(boundary))
assert.NoError(t, mw.WriteField("map_foo", "bar"))
req, _ := http.NewRequest("POST", "/?map_foo=getfoo", body)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
return req
@ -521,7 +546,7 @@ func createFormMultipartRequestFail() *http.Request {
func TestBindingFormPost(t *testing.T) {
req := createFormPostRequest()
var obj FooBarStruct
FormPost.Bind(req, &obj)
assert.NoError(t, FormPost.Bind(req, &obj))
assert.Equal(t, "form-urlencoded", FormPost.Name())
assert.Equal(t, "bar", obj.Foo)
@ -531,7 +556,7 @@ func TestBindingFormPost(t *testing.T) {
func TestBindingDefaultValueFormPost(t *testing.T) {
req := createDefaultFormPostRequest()
var obj FooDefaultBarStruct
FormPost.Bind(req, &obj)
assert.NoError(t, FormPost.Bind(req, &obj))
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, "hello", obj.Bar)
@ -545,9 +570,9 @@ func TestBindingFormPostFail(t *testing.T) {
}
func TestBindingFormMultipart(t *testing.T) {
req := createFormMultipartRequest()
req := createFormMultipartRequest(t)
var obj FooBarStruct
FormMultipart.Bind(req, &obj)
assert.NoError(t, FormMultipart.Bind(req, &obj))
assert.Equal(t, "multipart/form-data", FormMultipart.Name())
assert.Equal(t, "bar", obj.Foo)
@ -555,14 +580,14 @@ func TestBindingFormMultipart(t *testing.T) {
}
func TestBindingFormMultipartFail(t *testing.T) {
req := createFormMultipartRequestFail()
req := createFormMultipartRequestFail(t)
var obj FooStructForMapType
err := FormMultipart.Bind(req, &obj)
assert.Error(t, err)
}
func TestBindingProtoBuf(t *testing.T) {
test := &example.Test{
test := &protoexample.Test{
Label: proto.String("yes"),
}
data, _ := proto.Marshal(test)
@ -574,7 +599,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)
@ -645,6 +670,49 @@ func TestExistsFails(t *testing.T) {
assert.Error(t, err)
}
func TestUriBinding(t *testing.T) {
b := Uri
assert.Equal(t, "uri", b.Name())
type Tag struct {
Name string `uri:"name"`
}
var tag Tag
m := make(map[string][]string)
m["name"] = []string{"thinkerou"}
assert.NoError(t, b.BindUri(m, &tag))
assert.Equal(t, "thinkerou", tag.Name)
type NotSupportStruct struct {
Name map[string]interface{} `uri:"name"`
}
var not NotSupportStruct
assert.Error(t, b.BindUri(m, &not))
assert.Equal(t, map[string]interface{}(nil), not.Name)
}
func TestUriInnerBinding(t *testing.T) {
type Tag struct {
Name string `uri:"name"`
S struct {
Age int `uri:"age"`
}
}
expectedName := "mike"
expectedAge := 25
m := map[string][]string{
"name": {expectedName},
"age": {strconv.Itoa(expectedAge)},
}
var tag Tag
assert.NoError(t, Uri.BindUri(m, &tag))
assert.Equal(t, tag.Name, expectedName)
assert.Equal(t, tag.S.Age, expectedAge)
}
func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
@ -1156,14 +1224,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 +1247,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 +1255,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)
@ -1215,3 +1283,12 @@ func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return
}
func TestCanSet(t *testing.T) {
type CanSetStruct struct {
lowerStart string `form:"lower"`
}
var c CanSetStruct
assert.Nil(t, mapForm(&c, nil))
}

View File

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

View File

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

View File

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

View File

@ -6,10 +6,11 @@ package binding
import (
"bytes"
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin/json"
"github.com/gin-gonic/gin/internal/json"
)
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
@ -24,6 +25,9 @@ func (jsonBinding) Name() string {
}
func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
if req == nil || req.Body == nil {
return fmt.Errorf("invalid request")
}
return decodeJSON(req.Body, obj)
}

View File

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

18
binding/uri.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
type uriBinding struct{}
func (uriBinding) Name() string {
return "uri"
}
func (uriBinding) BindUri(m map[string][]string, obj interface{}) error {
if err := mapUri(obj, m); err != nil {
return err
}
return validate(obj)
}

35
binding/yaml.go Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"bytes"
"io"
"net/http"
"gopkg.in/yaml.v2"
)
type yamlBinding struct{}
func (yamlBinding) Name() string {
return "yaml"
}
func (yamlBinding) Bind(req *http.Request, obj interface{}) error {
return decodeYAML(req.Body, obj)
}
func (yamlBinding) BindBody(body []byte, obj interface{}) error {
return decodeYAML(bytes.NewReader(body), obj)
}
func decodeYAML(r io.Reader, obj interface{}) error {
decoder := yaml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj)
}

View File

@ -31,6 +31,7 @@ const (
MIMEPlain = binding.MIMEPlain
MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
MIMEYAML = binding.MIMEYAML
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
)
@ -104,8 +105,9 @@ func (c *Context) Handler() HandlerFunc {
// See example in GitHub.
func (c *Context) Next() {
c.index++
for s := int8(len(c.handlers)); c.index < s; c.index++ {
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
@ -159,16 +161,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 +362,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,8 +416,11 @@ func (c *Context) PostFormArray(key string) []string {
// a boolean value whether at least one value exists for the given key.
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request
req.ParseForm()
req.ParseMultipartForm(c.engine.MaxMultipartMemory)
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form array: %v", err)
}
}
if values := req.PostForm[key]; len(values) > 0 {
return values, true
}
@ -416,8 +432,52 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false
}
// PostFormMap returns a map for a given form key.
func (c *Context) PostFormMap(key string) map[string]string {
dicts, _ := c.GetPostFormMap(key)
return dicts
}
// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
req := c.Request
if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
if err != http.ErrNotMultipart {
debugPrint("error on parse multipart form map: %v", err)
}
}
dicts, exist := c.get(req.PostForm, key)
if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
dicts, exist = c.get(req.MultipartForm.Value, key)
}
return dicts, exist
}
// get is an internal method and returns a map which satisfy conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
dicts := make(map[string]string)
exist := false
for k, v := range m {
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
exist = true
dicts[k[i+1:][:j]] = v[0]
}
}
}
return dicts, exist
}
// FormFile returns the first file for the provided form key.
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
if c.Request.MultipartForm == nil {
if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
return nil, err
}
}
_, fh, err := c.Request.FormFile(name)
return fh, err
}
@ -442,8 +502,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
}
defer out.Close()
io.Copy(out, src)
return nil
_, err = io.Copy(out, src)
return err
}
// Bind checks the Content-Type to select a binding engine automatically,
@ -464,20 +524,40 @@ func (c *Context) BindJSON(obj interface{}) error {
return c.MustBindWith(obj, binding.JSON)
}
// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
func (c *Context) BindXML(obj interface{}) error {
return c.MustBindWith(obj, binding.XML)
}
// BindQuery is a shortcut for c.MustBindWith(obj, binding.Query).
func (c *Context) BindQuery(obj interface{}) error {
return c.MustBindWith(obj, binding.Query)
}
// MustBindWith binds the passed struct pointer using the specified binding engine.
// It will abort the request with HTTP 400 if any error ocurrs.
// See the binding package.
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
if err = c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(400, err).SetType(ErrorTypeBind)
}
// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
func (c *Context) BindYAML(obj interface{}) error {
return c.MustBindWith(obj, binding.YAML)
}
return
// BindUri binds the passed struct pointer using binding.Uri.
// It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj interface{}) error {
if err := c.ShouldBindUri(obj); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
return err
}
return nil
}
// MustBindWith binds the passed struct pointer using the specified binding engine.
// It will abort the request with HTTP 400 if any error occurs.
// See the binding package.
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
return err
}
return nil
}
// ShouldBind checks the Content-Type to select a binding engine automatically,
@ -498,11 +578,30 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
return c.ShouldBindWith(obj, binding.JSON)
}
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
func (c *Context) ShouldBindXML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.XML)
}
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Query)
}
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
func (c *Context) ShouldBindYAML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj interface{}) error {
m := make(map[string][]string)
for _, v := range c.Params {
m[v.Key] = []string{v.Value}
}
return binding.Uri.BindUri(m, obj)
}
// ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
@ -514,9 +613,7 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
//
// NOTE: This method reads the body before binding. So you should use
// ShouldBindWith for better performance if you need to call only once.
func (c *Context) ShouldBindBodyWith(
obj interface{}, bb binding.BindingBody,
) (err error) {
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) {
var body []byte
if cb, ok := c.Get(BodyBytesKey); ok {
if cbb, ok := cb.([]byte); ok {
@ -589,9 +686,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
@ -608,9 +705,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.
@ -654,6 +751,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)
@ -695,7 +793,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.
@ -704,6 +807,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{}) {
@ -715,6 +824,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})
@ -760,6 +874,7 @@ func (c *Context) SSEvent(name string, message interface{}) {
})
}
// Stream sends a streaming response.
func (c *Context) Stream(step func(w io.Writer) bool) {
w := c.Writer
clientGone := w.CloseNotify()
@ -781,6 +896,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
@ -790,6 +906,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:
@ -805,10 +922,11 @@ func (c *Context) Negotiate(code int, config Negotiate) {
c.XML(code, data)
default:
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server"))
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) // nolint: errcheck
}
}
// NegotiateFormat returns an acceptable Accept format.
func (c *Context) NegotiateFormat(offered ...string) string {
assert1(len(offered) > 0, "you must provide at least one offer")
@ -828,6 +946,7 @@ func (c *Context) NegotiateFormat(offered ...string) string {
return ""
}
// SetAccepted sets Accept header data.
func (c *Context) SetAccepted(formats ...string) {
c.Accepted = formats
}
@ -836,18 +955,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

17
context_17.go Normal file
View File

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

27
context_17_test.go Normal file
View File

@ -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": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}

View File

@ -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)
@ -64,7 +70,8 @@ func TestContextFormFile(t *testing.T) {
mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) {
w.Write([]byte("test"))
_, err = w.Write([]byte("test"))
assert.NoError(t, err)
}
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
@ -78,13 +85,27 @@ func TestContextFormFile(t *testing.T) {
assert.NoError(t, c.SaveUploadedFile(f, "test"))
}
func TestContextFormFileFailed(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
c.engine.MaxMultipartMemory = 8 << 20
f, err := c.FormFile("file")
assert.Error(t, err)
assert.Nil(t, f)
}
func TestContextMultipartForm(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.WriteField("foo", "bar")
assert.NoError(t, mw.WriteField("foo", "bar"))
w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) {
w.Write([]byte("test"))
_, err = w.Write([]byte("test"))
assert.NoError(t, err)
}
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
@ -118,7 +139,8 @@ func TestSaveUploadedCreateFailed(t *testing.T) {
mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) {
w.Write([]byte("test"))
_, err = w.Write([]byte("test"))
assert.NoError(t, err)
}
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
@ -140,7 +162,7 @@ func TestContextReset(t *testing.T) {
c.index = 2
c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()}
c.Params = Params{Param{}}
c.Error(errors.New("test"))
c.Error(errors.New("test")) // nolint: errcheck
c.Set("foo", "bar")
c.reset()
@ -371,7 +393,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 +462,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 +562,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 +604,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 +635,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": "<b>"})
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 +654,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 +680,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 +694,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 +707,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 +720,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 +732,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 +746,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 +758,22 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.SecureJSON(204, []string{"foo", "bar"})
c.SecureJSON(http.StatusNoContent, []string{"foo", "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextRenderNoContentAsciiJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.AsciiJSON(http.StatusNoContent, []string{"lang", "Go语言"})
assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String())
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
}
// Tests that the response executes the templates
@ -695,11 +785,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 +800,20 @@ func TestContextRenderHTML2(t *testing.T) {
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
assert.Len(t, router.trees, 1)
var b bytes.Buffer
setup(&b)
defer teardown()
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
re := captureOutput(t, func() {
SetMode(DebugMode)
router.SetHTMLTemplate(templ)
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", b.String())
assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", re)
c.HTML(201, "t", H{"name": "alexandernyquist"})
c.HTML(http.StatusCreated, "t", H{"name": "alexandernyquist"})
assert.Equal(t, 201, w.Code)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
// Tests that no HTML is rendered if code is 204
@ -733,11 +823,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 +836,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, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
// Tests that no XML is rendered if code is 204
@ -758,11 +848,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 +861,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 +873,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 +887,11 @@ func TestContextRenderHTMLString(t *testing.T) {
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(201, "<html>%s %d</html>", "string", 3)
c.String(http.StatusCreated, "<html>%s %d</html>", "string", 3)
assert.Equal(t, 201, w.Code)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "<html>string 3</html>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
// Tests that no HTML String is rendered if code is 204
@ -810,11 +900,11 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(204, "<html>%s %d</html>", "string", 3)
c.String(http.StatusNoContent, "<html>%s %d</html>", "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 +913,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 +925,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 +956,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 +967,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 +1023,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 +1034,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 +1046,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 +1071,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 +1086,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, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithHTML(t *testing.T) {
@ -987,15 +1103,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 +1119,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 +1147,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 +1181,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,18 +1203,19 @@ func TestContextAbortWithStatusJSON(t *testing.T) {
in.Bar = "barValue"
in.Foo = "fooValue"
c.AbortWithStatusJSON(415, in)
c.AbortWithStatusJSON(http.StatusUnsupportedMediaType, in)
assert.Equal(t, abortIndex, c.index)
assert.Equal(t, 415, c.Writer.Status())
assert.Equal(t, 415, w.Code)
assert.Equal(t, http.StatusUnsupportedMediaType, c.Writer.Status())
assert.Equal(t, http.StatusUnsupportedMediaType, w.Code)
assert.True(t, c.IsAborted())
contentType := w.Header().Get("Content-Type")
assert.Equal(t, "application/json; charset=utf-8", contentType)
buf := new(bytes.Buffer)
buf.ReadFrom(w.Body)
_, err := buf.ReadFrom(w.Body)
assert.NoError(t, err)
jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
}
@ -1107,11 +1224,11 @@ func TestContextError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
assert.Empty(t, c.Errors)
c.Error(errors.New("first error"))
c.Error(errors.New("first error")) // nolint: errcheck
assert.Len(t, c.Errors, 1)
assert.Equal(t, "Error #01: first error\n", c.Errors.String())
c.Error(&Error{
c.Error(&Error{ // nolint: errcheck
Err: errors.New("second error"),
Meta: "some data 2",
Type: ErrorTypePublic,
@ -1133,13 +1250,13 @@ func TestContextError(t *testing.T) {
t.Error("didn't panic")
}
}()
c.Error(nil)
c.Error(nil) // nolint: errcheck
}
func TestContextTypedError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) // nolint: errcheck
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) // nolint: errcheck
for _, err := range c.Errors.ByType(ErrorTypePublic) {
assert.Equal(t, ErrorTypePublic, err.Type)
@ -1154,9 +1271,9 @@ func TestContextAbortWithError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") // nolint: errcheck
assert.Equal(t, 401, w.Code)
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, abortIndex, c.index)
assert.True(t, c.IsAborted())
}
@ -1230,6 +1347,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(`<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
<bar>BAR</bar>
</root>`))
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()
@ -1247,6 +1384,23 @@ func TestContextBindWithQuery(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar"`
}
assert.NoError(t, c.BindYAML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBadAutoBind(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -1264,7 +1418,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,19 +1454,61 @@ func TestContextShouldBindWithJSON(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
<bar>BAR</bar>
</root>`))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `xml:"foo"`
Bar string `xml:"bar"`
}
assert.NoError(t, c.ShouldBindXML(&obj))
assert.Equal(t, "FOO", obj.Foo)
assert.Equal(t, "BAR", obj.Bar)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithQuery(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", bytes.NewBufferString("foo=unused"))
var obj struct {
Foo string `form:"foo"`
Bar string `form:"bar"`
Foo string `form:"foo"`
Bar string `form:"bar"`
Foo1 string `form:"Foo"`
Bar1 string `form:"Bar"`
}
assert.NoError(t, c.ShouldBindQuery(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, "foo1", obj.Bar1)
assert.Equal(t, "bar1", obj.Foo1)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("foo: bar\nbar: foo"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar"`
}
assert.NoError(t, c.ShouldBindYAML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
@ -1486,7 +1682,76 @@ 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
}()
_, err := w.Write([]byte("test"))
assert.NoError(t, err)
return stopStream
})
assert.Equal(t, "testtest", w.Body.String())
}
func TestContextStreamWithClientGone(t *testing.T) {
w := CreateTestResponseRecorder()
c, _ := CreateTestContext(w)
c.Stream(func(writer io.Writer) bool {
defer func() {
w.closeClient()
}()
_, err := writer.Write([]byte("test"))
assert.NoError(t, err)
return true
})
assert.Equal(t, "test", w.Body.String())
}
func TestContextResetInHandler(t *testing.T) {
w := CreateTestResponseRecorder()
c, _ := CreateTestContext(w)
c.handlers = []HandlerFunc{
func(c *Context) { c.reset() },
}
assert.NotPanics(t, func() {
c.Next()
})
}

View File

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

View File

@ -6,13 +6,15 @@ package gin
import (
"bytes"
"fmt"
"html/template"
"log"
"os"
"runtime"
"strconv"
"strings"
)
func init() {
log.SetFlags(0)
}
const ginSupportMinGoVer = 6
// IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -20,11 +22,18 @@ func IsDebugging() bool {
return ginMode == debugCode
}
// DebugPrintRouteFunc indicates debug log output format.
var DebugPrintRouteFunc func(httpMethod, absolutePath, handlerName string, nuHandlers int)
func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
if IsDebugging() {
nuHandlers := len(handlers)
handlerName := nameOfFunction(handlers.Last())
debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
if DebugPrintRouteFunc == nil {
debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
} else {
DebugPrintRouteFunc(httpMethod, absolutePath, handlerName, nuHandlers)
}
}
}
@ -42,11 +51,28 @@ func debugPrintLoadTemplate(tmpl *template.Template) {
func debugPrint(format string, values ...interface{}) {
if IsDebugging() {
log.Printf("[GIN-debug] "+format, values...)
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(os.Stderr, "[GIN-debug] "+format, values...)
}
}
func getMinVer(v string) (uint64, error) {
first := strings.IndexByte(v, '.')
last := strings.LastIndexByte(v, '.')
if first == last {
return strconv.ParseUint(v[first+1:], 10, 64)
}
return strconv.ParseUint(v[first+1:last], 10, 64)
}
func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
`)
}
debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
`)

View File

@ -11,6 +11,8 @@ import (
"io"
"log"
"os"
"runtime"
"sync"
"testing"
"github.com/stretchr/testify/assert"
@ -30,86 +32,122 @@ func TestIsDebugging(t *testing.T) {
}
func TestDebugPrint(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
SetMode(ReleaseMode)
debugPrint("DEBUG this!")
SetMode(TestMode)
debugPrint("DEBUG this!")
assert.Empty(t, w.String())
SetMode(DebugMode)
debugPrint("these are %d %s\n", 2, "error messages")
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", w.String())
re := captureOutput(t, func() {
SetMode(DebugMode)
SetMode(ReleaseMode)
debugPrint("DEBUG this!")
SetMode(TestMode)
debugPrint("DEBUG this!")
SetMode(DebugMode)
debugPrint("these are %d %s", 2, "error messages")
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] these are 2 error messages\n", re)
}
func TestDebugPrintError(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
SetMode(DebugMode)
debugPrintError(nil)
assert.Empty(t, w.String())
debugPrintError(errors.New("this is an error"))
assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", w.String())
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintError(nil)
debugPrintError(errors.New("this is an error"))
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [ERROR] this is an error\n", re)
}
func TestDebugPrintRoutes(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, w.String())
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintRoute("GET", "/path/to/route/:param", HandlersChain{func(c *Context) {}, handlerNameTest})
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
}
func TestDebugPrintLoadTemplate(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl"))
debugPrintLoadTemplate(templ)
assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, w.String())
re := captureOutput(t, func() {
SetMode(DebugMode)
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
debugPrintLoadTemplate(templ)
SetMode(TestMode)
})
assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, re)
}
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
debugPrintWARNINGSetHTMLTemplate()
assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", w.String())
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGSetHTMLTemplate()
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n", re)
}
func TestDebugPrintWARNINGDefault(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
debugPrintWARNINGDefault()
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", w.String())
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGDefault()
SetMode(TestMode)
})
m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
}
}
func TestDebugPrintWARNINGNew(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
debugPrintWARNINGNew()
assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", w.String())
re := captureOutput(t, func() {
SetMode(DebugMode)
debugPrintWARNINGNew()
SetMode(TestMode)
})
assert.Equal(t, "[GIN-debug] [WARNING] Running in \"debug\" mode. Switch to \"release\" mode in production.\n - using env:\texport GIN_MODE=release\n - using code:\tgin.SetMode(gin.ReleaseMode)\n\n", re)
}
func setup(w io.Writer) {
SetMode(DebugMode)
log.SetOutput(w)
func captureOutput(t *testing.T, f func()) string {
reader, writer, err := os.Pipe()
if err != nil {
panic(err)
}
stdout := os.Stdout
stderr := os.Stderr
defer func() {
os.Stdout = stdout
os.Stderr = stderr
log.SetOutput(os.Stderr)
}()
os.Stdout = writer
os.Stderr = writer
log.SetOutput(writer)
out := make(chan string)
wg := new(sync.WaitGroup)
wg.Add(1)
go func() {
var buf bytes.Buffer
wg.Done()
_, err := io.Copy(&buf, reader)
assert.NoError(t, err)
out <- buf.String()
}()
wg.Wait()
f()
writer.Close()
return <-out
}
func teardown() {
SetMode(TestMode)
log.SetOutput(os.Stdout)
func TestGetMinVer(t *testing.T) {
var m uint64
var e error
_, e = getMinVer("go1")
assert.NotNil(t, e)
m, e = getMinVer("go1.1")
assert.Equal(t, uint64(1), m)
assert.Nil(t, e)
m, e = getMinVer("go1.1.1")
assert.Nil(t, e)
assert.Equal(t, uint64(1), m)
_, e = getMinVer("go1.1.1.1")
assert.NotNil(t, e)
}

View File

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

View File

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

View File

@ -9,21 +9,28 @@ import (
"fmt"
"reflect"
"github.com/gin-gonic/gin/json"
"github.com/gin-gonic/gin/internal/json"
)
// ErrorType is an unsigned 64-bit error code as defined in the gin spec.
type ErrorType uint64
const (
ErrorTypeBind ErrorType = 1 << 63 // used when c.Bind() fails
ErrorTypeRender ErrorType = 1 << 62 // used when c.Render() fails
// ErrorTypeBind is used when Context.Bind() fails.
ErrorTypeBind ErrorType = 1 << 63
// ErrorTypeRender is used when Context.Render() fails.
ErrorTypeRender ErrorType = 1 << 62
// ErrorTypePrivate indicates a private error.
ErrorTypePrivate ErrorType = 1 << 0
ErrorTypePublic ErrorType = 1 << 1
// ErrorTypePublic indicates a public error.
ErrorTypePublic ErrorType = 1 << 1
// ErrorTypeAny indicates any other error.
ErrorTypeAny ErrorType = 1<<64 - 1
ErrorTypeNu = 2
// ErrorTypeNu indicates any other error.
ErrorTypeNu = 2
)
// Error represents a error's specification.
type Error struct {
Err error
Type ErrorType
@ -34,16 +41,19 @@ type errorMsgs []*Error
var _ error = &Error{}
// SetType sets the error's type.
func (msg *Error) SetType(flags ErrorType) *Error {
msg.Type = flags
return msg
}
// SetMeta sets the error's meta data.
func (msg *Error) SetMeta(data interface{}) *Error {
msg.Meta = data
return msg
}
// JSON creates a properly formated JSON
func (msg *Error) JSON() interface{} {
json := H{}
if msg.Meta != nil {
@ -70,11 +80,12 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(msg.JSON())
}
// Error implements the error interface
// Error implements the error interface.
func (msg Error) Error() string {
return msg.Err.Error()
}
// IsType judges one error.
func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0
}
@ -138,6 +149,7 @@ func (a errorMsgs) JSON() interface{} {
}
}
// MarshalJSON implements the json.Marshaller interface.
func (a errorMsgs) MarshalJSON() ([]byte, error) {
return json.Marshal(a.JSON())
}

View File

@ -8,7 +8,7 @@ import (
"errors"
"testing"
"github.com/gin-gonic/gin/json"
"github.com/gin-gonic/gin/internal/json"
"github.com/stretchr/testify/assert"
)
@ -19,47 +19,47 @@ func TestError(t *testing.T) {
Type: ErrorTypePrivate,
}
assert.Equal(t, err.Error(), baseError.Error())
assert.Equal(t, err.JSON(), H{"error": baseError.Error()})
assert.Equal(t, H{"error": baseError.Error()}, err.JSON())
assert.Equal(t, err.SetType(ErrorTypePublic), err)
assert.Equal(t, err.Type, ErrorTypePublic)
assert.Equal(t, ErrorTypePublic, err.Type)
assert.Equal(t, err.SetMeta("some data"), err)
assert.Equal(t, err.Meta, "some data")
assert.Equal(t, err.JSON(), H{
assert.Equal(t, "some data", err.Meta)
assert.Equal(t, H{
"error": baseError.Error(),
"meta": "some data",
})
}, err.JSON())
jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
err.SetMeta(H{
err.SetMeta(H{ // nolint: errcheck
"status": "200",
"data": "some data",
})
assert.Equal(t, err.JSON(), H{
assert.Equal(t, H{
"error": baseError.Error(),
"status": "200",
"data": "some data",
})
}, err.JSON())
err.SetMeta(H{
err.SetMeta(H{ // nolint: errcheck
"error": "custom error",
"status": "200",
"data": "some data",
})
assert.Equal(t, err.JSON(), H{
assert.Equal(t, H{
"error": "custom error",
"status": "200",
"data": "some data",
})
}, err.JSON())
type customError struct {
status string
data string
}
err.SetMeta(customError{status: "200", data: "other data"})
err.SetMeta(customError{status: "200", data: "other data"}) // nolint: errcheck
assert.Equal(t, customError{status: "200", data: "other data"}, err.JSON())
}

View File

@ -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"})
}
})

View File

@ -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())
}

View File

@ -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"`

View File

@ -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")
}

View File

@ -8,6 +8,7 @@ import (
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
@ -35,7 +36,10 @@ func main() {
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
// kill (no param) default send syscanll.SIGTERM
// kill -2 is syscall.SIGINT
// kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutdown Server ...")
@ -44,5 +48,10 @@ func main() {
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
// catching ctx.Done(). timeout of 5 seconds.
select {
case <-ctx.Done():
log.Println("timeout of 5 seconds.")
}
log.Println("Server exiting")
}

View File

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

View File

@ -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",
})
})

View File

@ -0,0 +1,30 @@
The [New Relic Go Agent](https://github.com/newrelic/go-agent) provides a nice middleware for the stdlib handler signature.
The following is an adaptation of that middleware for Gin.
```golang
const (
// NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context
NewRelicTxnKey = "NewRelicTxnKey"
)
// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler
func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc {
return func(ctx *gin.Context) {
txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request)
defer txn.End()
ctx.Set(NewRelicTxnKey, txn)
ctx.Next()
}
}
```
and in `main.go` or equivalent...
```golang
router := gin.Default()
cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY"))
app, err := newrelic.NewApplication(cfg)
if err != nil {
log.Printf("failed to make new_relic app: %v", err)
} else {
router.Use(adapters.NewRelicMonitoring(app))
}
```

View File

@ -0,0 +1,42 @@
package main
import (
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/newrelic/go-agent"
)
const (
// NewRelicTxnKey is the key used to retrieve the NewRelic Transaction from the context
NewRelicTxnKey = "NewRelicTxnKey"
)
// NewRelicMonitoring is a middleware that starts a newrelic transaction, stores it in the context, then calls the next handler
func NewRelicMonitoring(app newrelic.Application) gin.HandlerFunc {
return func(ctx *gin.Context) {
txn := app.StartTransaction(ctx.Request.URL.Path, ctx.Writer, ctx.Request)
defer txn.End()
ctx.Set(NewRelicTxnKey, txn)
ctx.Next()
}
}
func main() {
router := gin.Default()
cfg := newrelic.NewConfig(os.Getenv("APP_NAME"), os.Getenv("NEW_RELIC_API_KEY"))
app, err := newrelic.NewApplication(cfg)
if err != nil {
log.Printf("failed to make new_relic app: %v", err)
} else {
router.Use(NewRelicMonitoring(app))
}
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Hello World!\n")
})
router.Run()
}

View File

@ -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)

View File

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

View File

@ -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) {

View File

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

View File

@ -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,
})

View File

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

View File

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

View File

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

View File

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

141
gin.go
View File

@ -5,6 +5,7 @@
package gin
import (
"fmt"
"html/template"
"net"
"net/http"
@ -14,11 +15,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 +23,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 +37,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 +158,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 +175,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 +268,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 {
@ -316,6 +322,23 @@ func (engine *Engine) RunUnix(file string) (err error) {
return
}
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) RunFd(fd int) (err error) {
debugPrint("Listening and serving HTTP on fd@%d", fd)
defer func() { debugPrintError(err) }()
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd@%d", fd))
listener, err := net.FileListener(f)
if err != nil {
return
}
defer listener.Close()
err = http.Serve(listener, engine)
return
}
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
@ -332,9 +355,11 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// This can be done by setting c.Request.URL.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
oldIndexValue := c.index
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
c.index = oldIndexValue
}
func (engine *Engine) handleHTTPRequest(c *Context) {
@ -349,43 +374,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 +420,32 @@ 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
_, err := c.Writer.Write(defaultMessage)
if err != nil {
debugPrint("cannot write message to writer during serve error: %v", err)
}
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 +456,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())

View File

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

View File

@ -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()
@ -62,7 +87,7 @@ func TestRunEmptyWithEnv(t *testing.T) {
func TestRunTooMuchParams(t *testing.T) {
router := New()
assert.Panics(t, func() {
router.Run("2", "2")
assert.NoError(t, router.Run("2", "2"))
})
}
@ -109,6 +134,42 @@ func TestBadUnixSocket(t *testing.T) {
assert.Error(t, router.RunUnix("#/tmp/unix_unit_test"))
}
func TestFileDescriptor(t *testing.T) {
router := New()
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
assert.NoError(t, err)
listener, err := net.ListenTCP("tcp", addr)
assert.NoError(t, err)
socketFile, err := listener.File()
assert.NoError(t, err)
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
assert.NoError(t, router.RunFd(int(socketFile.Fd())))
}()
// have to wait for the goroutine to start and run the server
// otherwise the main thread will complete
time.Sleep(5 * time.Millisecond)
c, err := net.Dial("tcp", listener.Addr().String())
assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c)
var response string
for scanner.Scan() {
response += scanner.Text()
}
assert.Contains(t, response, "HTTP/1.0 200", "should get a 200")
assert.Contains(t, response, "it worked", "resp body should match")
}
func TestBadFileDescriptor(t *testing.T) {
router := New()
assert.Error(t, router.RunFd(0))
}
func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
router := New()
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -119,6 +180,26 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
testRequest(t, ts.URL+"/example")
}
func TestConcurrentHandleContext(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.Request.URL.Path = "/example"
router.HandleContext(c)
})
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
var wg sync.WaitGroup
iterations := 200
wg.Add(iterations)
for i := 0; i < iterations; i++ {
go func() {
testGetRequestHandler(t, router, "/")
wg.Done()
}()
}
wg.Wait()
}
// func TestWithHttptestWithSpecifiedPort(t *testing.T) {
// router := New()
// router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -133,3 +214,14 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
// testRequest(t, "http://localhost:8033/example")
// }
func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
req, err := http.NewRequest("GET", url, nil)
assert.NoError(t, err)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
assert.Equal(t, 200, w.Code, "should get a 200")
}

View File

@ -10,7 +10,10 @@ import (
"html/template"
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"
"strconv"
"sync/atomic"
"testing"
"time"
@ -22,15 +25,18 @@ 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()
func setupHTMLFiles(t *testing.T, mode string, tls bool, loadMethod func(*Engine)) *httptest.Server {
SetMode(mode)
defer SetMode(TestMode)
var router *Engine
captureOutput(t, func() {
router = New()
router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
router.LoadHTMLFiles("./fixtures/basic/hello.tmpl", "./fixtures/basic/raw.tmpl")
loadMethod(router)
router.GET("/test", func(c *Context) {
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
})
@ -39,88 +45,90 @@ func setupHTMLFiles(t *testing.T, mode string, tls bool) func() {
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
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 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() {}
}
func TestLoadHTMLGlobDebugMode(t *testing.T) {
ts := setupHTMLFiles(
t,
DebugMode,
false,
func(router *Engine) {
router.LoadHTMLGlob("./testdata/template/*")
},
)
defer ts.Close()
func TestLoadHTMLGlob(t *testing.T) {
td := setupHTMLGlob(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
assert.Equal(t, "<h1>Hello world</h1>", 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, "<h1>Hello world</h1>", string(resp[:]))
td()
assert.Equal(t, "<h1>Hello world</h1>", 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, "<h1>Hello world</h1>", string(resp[:]))
td()
assert.Equal(t, "<h1>Hello world</h1>", 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 +136,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, "<h1>Hello world</h1>", string(resp[:]))
td()
assert.Equal(t, "<h1>Hello world</h1>", 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 +176,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 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()
func TestLoadHTMLFiles(t *testing.T) {
td := setupHTMLFiles(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
res, err := http.Get(fmt.Sprintf("%s/test", ts.URL))
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
assert.Equal(t, "<h1>Hello world</h1>", 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, "<h1>Hello world</h1>", string(resp[:]))
td()
assert.Equal(t, "<h1>Hello world</h1>", 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, "<h1>Hello world</h1>", string(resp[:]))
td()
assert.Equal(t, "<h1>Hello world</h1>", 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 +254,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, "<h1>Hello world</h1>", string(resp[:]))
td()
assert.Equal(t, "<h1>Hello world</h1>", 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) {
@ -443,6 +478,60 @@ func TestListOfRoutes(t *testing.T) {
})
}
func TestEngineHandleContext(t *testing.T) {
r := New()
r.GET("/", func(c *Context) {
c.Request.URL.Path = "/v2"
r.HandleContext(c)
})
v2 := r.Group("/v2")
{
v2.GET("/", func(c *Context) {})
}
assert.NotPanics(t, func() {
w := performRequest(r, "GET", "/")
assert.Equal(t, 301, w.Code)
})
}
func TestEngineHandleContextManyReEntries(t *testing.T) {
expectValue := 10000
var handlerCounter, middlewareCounter int64
r := New()
r.Use(func(c *Context) {
atomic.AddInt64(&middlewareCounter, 1)
})
r.GET("/:count", func(c *Context) {
countStr := c.Param("count")
count, err := strconv.Atoi(countStr)
assert.NoError(t, err)
n, err := c.Writer.Write([]byte("."))
assert.NoError(t, err)
assert.Equal(t, 1, n)
switch {
case count > 0:
c.Request.URL.Path = "/" + strconv.Itoa(count-1)
r.HandleContext(c)
}
}, func(c *Context) {
atomic.AddInt64(&handlerCounter, 1)
})
assert.NotPanics(t, func() {
w := performRequest(r, "GET", "/"+strconv.Itoa(expectValue-1)) // include 0 value
assert.Equal(t, 200, w.Code)
assert.Equal(t, expectValue, w.Body.Len())
})
assert.Equal(t, int64(expectValue), handlerCounter)
assert.Equal(t, int64(expectValue), middlewareCounter)
}
func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo) {
for _, gotRoute := range gotRoutes {
if gotRoute.Path == wantRoute.Path && gotRoute.Method == wantRoute.Method {

View File

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

32
go.mod Normal file
View File

@ -0,0 +1,32 @@
module github.com/gin-gonic/gin
require (
github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482
github.com/golang/protobuf v1.2.0
github.com/json-iterator/go v1.1.5
github.com/mattn/go-isatty v0.0.4
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/stretchr/testify v1.3.0
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e // indirect
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2
gopkg.in/yaml.v2 v2.2.2
)
exclude (
github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160
github.com/client9/misspell v0.3.4
github.com/dustin/go-broadcast v0.0.0-20171205050544-f664265f5a66
github.com/gin-gonic/autotls v0.0.0-20190119125636-0b5f4fc15768
github.com/jessevdk/go-assets v0.0.0-20160921144138-4f4301a06e15
github.com/manucorporat/stats v0.0.0-20180402194714-3ba42d56d227
github.com/newrelic/go-agent v2.5.0+incompatible
github.com/thinkerou/favicon v0.1.0
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b
golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1
google.golang.org/grpc v1.18.0
)

54
go.sum Normal file
View File

@ -0,0 +1,54 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160 h1:HJpuhXOHC4EkXDARsLjmXAV9FhlY6qFDnKI/MJM6eoE=
github.com/campoy/embedmd v0.0.0-20181127031020-97c13d6e4160/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482 h1:iOz5sIQUvuOlpiC7Q6+MmJQpWnlneYX98QIGf+2m50Y=
github.com/gin-contrib/sse v0.0.0-20190124093953-61b50c2ef482/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e h1:MDa3fSUp6MdYHouVmCCNz/zaH2a6CRcxY3VhT/K3C5Q=
golang.org/x/net v0.0.0-20190119204137-ed066c81e75e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e h1:3GIlrlVLfkoipSReOMNAgApI0ajnalyLa/EZHHca/XI=
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

20
internal/json/json.go Normal file
View File

@ -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
)

21
internal/json/jsoniter.go Normal file
View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

136
logger.go
View File

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

View File

@ -7,7 +7,10 @@ package gin
import (
"bytes"
"errors"
"fmt"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
@ -78,13 +81,185 @@ func TestLogger(t *testing.T) {
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound")
}
func TestLoggerWithConfig(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{Output: buffer}))
router.GET("/example", func(c *Context) {})
router.POST("/example", func(c *Context) {})
router.PUT("/example", func(c *Context) {})
router.DELETE("/example", func(c *Context) {})
router.PATCH("/example", func(c *Context) {})
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
performRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/notfound")
}
func TestLoggerWithFormatter(t *testing.T) {
buffer := new(bytes.Buffer)
d := DefaultWriter
DefaultWriter = buffer
defer func() {
DefaultWriter = d
}()
router := New()
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode,
param.Latency,
param.ClientIP,
param.Method,
param.Path,
param.ErrorMessage,
)
}))
router.GET("/example", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
}
func TestLoggerWithConfigFormatting(t *testing.T) {
var gotParam LogFormatterParams
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
Formatter: func(param LogFormatterParams) string {
// for assert test
gotParam = param
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
param.StatusCode,
param.Latency,
param.ClientIP,
param.Method,
param.Path,
param.ErrorMessage,
)
},
}))
router.GET("/example", func(c *Context) {
// set dummy ClientIP
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
})
performRequest(router, "GET", "/example?a=100")
// output test
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "a=100")
// LogFormatterParams test
assert.NotNil(t, gotParam.Request)
assert.NotEmpty(t, gotParam.TimeStamp)
assert.Equal(t, 200, gotParam.StatusCode)
assert.NotEmpty(t, gotParam.Latency)
assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
assert.Equal(t, "GET", gotParam.Method)
assert.Equal(t, "/example?a=100", gotParam.Path)
assert.Empty(t, gotParam.ErrorMessage)
}
func TestDefaultLogFormatter(t *testing.T) {
timeStamp := time.Unix(1544173902, 0).UTC()
termFalseParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
IsTerm: false,
}
termTrueParam := LogFormatterParams{
TimeStamp: timeStamp,
StatusCode: 200,
Latency: time.Second * 5,
ClientIP: "20.20.20.20",
Method: "GET",
Path: "/",
ErrorMessage: "",
IsTerm: true,
}
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam))
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam))
}
func TestColorForMethod(t *testing.T) {
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 +268,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")
}
@ -103,30 +278,30 @@ func TestErrorLogger(t *testing.T) {
router := New()
router.Use(ErrorLogger())
router.GET("/error", func(c *Context) {
c.Error(errors.New("this is an error"))
c.Error(errors.New("this is an error")) // nolint: errcheck
})
router.GET("/abort", func(c *Context) {
c.AbortWithError(401, errors.New("no authorized"))
c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized")) // nolint: errcheck
})
router.GET("/print", func(c *Context) {
c.Error(errors.New("this is an error"))
c.String(500, "hola!")
c.Error(errors.New("this is an error")) // nolint: errcheck
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())
}
func TestSkippingPaths(t *testing.T) {
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithWriter(buffer, "/skipped"))
@ -141,6 +316,24 @@ func TestSkippingPaths(t *testing.T) {
assert.Contains(t, buffer.String(), "")
}
func TestLoggerWithConfigSkippingPaths(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithConfig(LoggerConfig{
Output: buffer,
SkipPaths: []string{"/skipped"},
}))
router.GET("/logged", func(c *Context) {})
router.GET("/skipped", func(c *Context) {})
performRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
performRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "")
}
func TestDisableConsoleColor(t *testing.T) {
New()
assert.False(t, disableColor)

View File

@ -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")) // nolint: errcheck
})
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<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
}

17
mode.go
View File

@ -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 release.
ReleaseMode = "release"
TestMode = "test"
// TestMode indicates gin mode is test.
TestMode = "test"
)
const (
debugCode = iota
@ -24,7 +28,7 @@ const (
testCode
)
// DefaultWriter is the default io.Writer used the Gin for debug output and
// DefaultWriter is the default io.Writer used by Gin for debug output and
// middleware output like Logger() or Recovery().
// Note that both Logger and Recovery provides custom ways to configure their
// output io.Writer.
@ -32,6 +36,8 @@ const (
// import "github.com/mattn/go-colorable"
// gin.DefaultWriter = colorable.NewColorableStdout()
var DefaultWriter io.Writer = os.Stdout
// DefaultErrorWriter is the default io.Writer used by Gin to debug errors
var DefaultErrorWriter io.Writer = os.Stderr
var ginMode = debugCode
@ -42,6 +48,7 @@ func init() {
SetMode(mode)
}
// SetMode sets gin mode according to input string.
func SetMode(value string) {
switch value {
case DebugMode, "":
@ -59,14 +66,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
}

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ package render
import "net/http"
// 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})
}

View File

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

90
render/json.go Executable file → Normal file
View File

@ -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,34 +55,39 @@ 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)
if err != nil {
return err
}
w.Write(jsonBytes)
return nil
_, err = w.Write(jsonBytes)
return err
}
// 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, "", " ")
if err != nil {
return err
}
w.Write(jsonBytes)
return nil
_, err = w.Write(jsonBytes)
return err
}
// 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)
@ -78,16 +96,21 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
}
// if the jsonBytes is array values
if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) {
w.Write([]byte(r.Prefix))
_, err = w.Write([]byte(r.Prefix))
if err != nil {
return err
}
}
w.Write(jsonBytes)
return nil
_, err = w.Write(jsonBytes)
return err
}
// 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)
@ -96,19 +119,58 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
}
if r.Callback == "" {
w.Write(ret)
return nil
_, err = w.Write(ret)
return err
}
callback := template.JSEscapeString(r.Callback)
w.Write([]byte(callback))
w.Write([]byte("("))
w.Write(ret)
w.Write([]byte(")"))
_, err = w.Write([]byte(callback))
if err != nil {
return err
}
_, err = w.Write([]byte("("))
if err != nil {
return err
}
_, err = w.Write(ret)
if err != nil {
return err
}
_, err = w.Write([]byte(")"))
if err != nil {
return err
}
return nil
}
// 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)
}
_, err = w.Write(buffer.Bytes())
return err
}
// WriteContentType (AsciiJSON) writes JSON ContentType.
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType)
}

31
render/json_17.go Normal file
View File

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

View File

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

36
render/protobuf.go Normal file
View File

@ -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
}
_, err = w.Write(bytes)
return err
}
// WriteContentType (ProtoBuf) writes ProtoBuf ContentType.
func (r ProtoBuf) WriteContentType(w http.ResponseWriter) {
writeContentType(w, protobufContentType)
}

View File

@ -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 {

View File

@ -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) {}

5
render/render.go Executable file → Normal file
View File

@ -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) {

26
render/render_17_test.go Normal file
View File

@ -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": "<b>",
}
err := (PureJSON{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}

99
render/render_test.go Executable file → Normal file
View File

@ -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": "<b>",
}
(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"))
}
@ -67,7 +71,7 @@ func TestRenderJSONPanics(t *testing.T) {
data := make(chan int)
// json: unsupported type: chan int
assert.Panics(t, func() { (JSON{data}).Render(w) })
assert.Panics(t, func() { assert.NoError(t, (JSON{data}).Render(w)) })
}
func TestRenderIndentedJSON(t *testing.T) {
@ -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": "<br>",
}
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,13 +329,13 @@ func TestRenderRedirect(t *testing.T) {
assert.NoError(t, err)
data2 := Redirect{
Code: 200,
Code: http.StatusOK,
Request: req,
Location: "/new/location",
}
w = httptest.NewRecorder()
assert.Panics(t, func() { data2.Render(w) })
assert.Panics(t, func() { assert.NoError(t, data2.Render(w)) })
// only improve coverage
data2.WriteContentType(w)
@ -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"))
}

View File

@ -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
return WriteString(w, r.Format, r.Data)
}
// WriteContentType (String) writes Plain ContentType.
func (r String) WriteContentType(w http.ResponseWriter) {
writeContentType(w, plainContentType)
}
func WriteString(w http.ResponseWriter, format string, data []interface{}) {
// WriteString writes data according to its format and write custom ContentType.
func WriteString(w http.ResponseWriter, format string, data []interface{}) (err error) {
writeContentType(w, plainContentType)
if len(data) > 0 {
fmt.Fprintf(w, format, data...)
} else {
io.WriteString(w, format)
_, err = fmt.Fprintf(w, format, data...)
return
}
_, err = io.WriteString(w, format)
return
}

View File

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

View File

@ -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)
@ -24,10 +26,11 @@ func (r YAML) Render(w http.ResponseWriter) error {
return err
}
w.Write(bytes)
return nil
_, err = w.Write(bytes)
return err
}
// WriteContentType (YAML) writes YAML ContentType for response.
func (r YAML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, yamlContentType)
}

View File

@ -13,7 +13,7 @@ import (
const (
noWritten = -1
defaultStatus = 200
defaultStatus = http.StatusOK
)
type responseWriterBase interface {
@ -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()
}

View File

@ -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)
@ -103,7 +103,8 @@ func TestResponseWriterHijack(t *testing.T) {
w := ResponseWriter(writer)
assert.Panics(t, func() {
w.Hijack()
_, _, err := w.Hijack()
assert.NoError(t, err)
})
assert.True(t, w.Written())
@ -113,3 +114,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)
}

View File

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

View File

@ -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())
}

View File

@ -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)
@ -251,7 +251,8 @@ func TestRouteStaticFile(t *testing.T) {
t.Error(err)
}
defer os.Remove(f.Name())
f.WriteString("Gin Web Framework")
_, err = f.WriteString("Gin Web Framework")
assert.NoError(t, err)
f.Close()
dir, filename := filepath.Split(f.Name())
@ -265,15 +266,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 +284,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 +296,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 +311,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 +334,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 +371,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 +392,49 @@ 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 TestRouterStaticFSNotFound(t *testing.T) {
router := New()
router.StaticFS("/", http.FileSystem(http.Dir("/thisreallydoesntexist/")))
router.NoRoute(func(c *Context) {
c.String(404, "non existent")
})
w := performRequest(router, "GET", "/nonexistent")
assert.Equal(t, "non existent", w.Body.String())
w = performRequest(router, "HEAD", "/nonexistent")
assert.Equal(t, "non existent", w.Body.String())
}
func TestRouterStaticFSFileNotFound(t *testing.T) {
router := New()
router.StaticFS("/", http.FileSystem(http.Dir(".")))
assert.NotPanics(t, func() {
performRequest(router, "GET", "/nonexistent")
})
}
func TestRouteRawPath(t *testing.T) {
@ -417,7 +453,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 +473,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) {

View File

@ -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) {

View File

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

View File

@ -1,4 +1,4 @@
package example;
package protoexample;
enum FOO {X=17;};

View File

@ -192,10 +192,8 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
continue walk
} else {
// Wildcard conflict
var pathSeg string
if n.nType == catchAll {
pathSeg = path
} else {
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path

View File

@ -12,7 +12,7 @@ import (
"testing"
)
// Used as a workaround since we can't compare functions or their addressses
// Used as a workaround since we can't compare functions or their addresses
var fakeHandlerValue string
func fakeHandler(val string) HandlersChain {
@ -127,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},
@ -170,23 +168,21 @@ 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"}}},
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}},
{"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{Key: "tool", Value: "test"}, Param{Key: "sub", Value: "3"}}},
{"/src/", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/", false, "/search/", nil},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
{"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}},
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}},
{"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
{"/user_gopher/about", false, "/user_:name/about", Params{Param{Key: "name", Value: "gopher"}}},
{"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{Key: "dir", Value: "js"}, Param{Key: "filepath", Value: "/inc/framework.js"}}},
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
})
checkPriorities(t, tree)
@ -210,22 +206,21 @@ func TestUnescapeParameters(t *testing.T) {
tree.addRoute(route, fakeHandler(route))
}
//printChildren(tree, "")
unescape := true
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}},
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}},
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}},
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}},
{"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}},
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}},
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},
{"/info/slash%%%%", false, "/info/:user", Params{Param{Key: "user", Value: "slash%%%%"}}},
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash%%%%2Fgordon"}, Param{Key: "project", Value: "Project%%%%20%231"}}},
}, unescape)
checkPriorities(t, tree)
@ -262,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) {
@ -330,14 +323,12 @@ func TestTreeDupliatePath(t *testing.T) {
}
}
//printChildren(tree, "")
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
{"/doc/", false, "/doc/", nil},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/user_gopher", false, "/user_:name", Params{Param{Key: "name", Value: "gopher"}}},
})
}
@ -446,8 +437,6 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
}
}
//printChildren(tree, "")
tsrRoutes := [...]string{
"/hi/",
"/b",

View File

@ -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)

View File

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

83
vendor/vendor.json vendored
View File

@ -1,13 +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"
"revision": "8991bc29aa16c548c550c7ff78260e27b9ab7c73",
"revisionTime": "2018-02-21T22:46:20Z",
"version": "v1.1",
"versionExact": "v1.1.1"
},
{
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
@ -16,56 +17,72 @@
"revisionTime": "2017-01-09T09:34:21Z"
},
{
"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": "Ajh8TemnItg4nn+jKmVcsMRALBc=",
"checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=",
"path": "github.com/json-iterator/go",
"revision": "36b14963da70d11297d313183d7e6388c8510e1e",
"revisionTime": "2017-08-29T15:58:51Z"
"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"
"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"
"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"
"revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab",
"revisionTime": "2018-04-07T10:07:33Z",
"version": "v1.1",
"versionExact": "v1.1.1"
},
{
"checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
"comment": "release-branch.go1.7",
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
"path": "golang.org/x/net/context",
"revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a",
"revisionTime": "2016-10-18T08:54:36Z"
"revision": "49bb7cea24b1df9410e1712aa6433dae904ff66a",
"revisionTime": "2018-10-11T05:27:23Z"
},
{
"checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=",
"comment": "v8.18.1",
"checksumSHA1": "SiJNkx+YGtq3Gtr6Ldu6OW83O+U=",
"path": "golang.org/x/sys/unix",
"revision": "fa43e7bc11baaae89f3f902b2b4d832b68234844",
"revisionTime": "2018-10-11T14:35:51Z"
},
{
"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"

8
version.go Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More