Merge remote-tracking branch 'upstream/master' into PureJSON

This commit is contained in:
Filip Figiel 2018-08-20 08:52:05 +02:00
commit ee988b59f2
102 changed files with 4321 additions and 861 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.
- gin version (or commit ref):
- git version:
- 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.

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ vendor/*
!vendor/vendor.json !vendor/vendor.json
coverage.out coverage.out
count.out count.out
test

View File

@ -5,10 +5,11 @@ go:
- 1.7.x - 1.7.x
- 1.8.x - 1.8.x
- 1.9.x - 1.9.x
- 1.10.x
- master - master
git: git:
depth: 3 depth: 10
install: install:
- make install - make install

View File

@ -1,8 +1,12 @@
List of all the awesome people working to make Gin the best Web Framework in Go. 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 ## 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. People and companies, who have contributed, in alphabetical order.

View File

@ -1,6 +1,28 @@
# CHANGELOG # 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] Switch from godeps to govendor
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls - [NEW] Add support for Let's Encrypt via gin-gonic/autotls

View File

@ -1,15 +1,16 @@
GOFMT ?= gofmt "-s" GOFMT ?= gofmt "-s"
PACKAGES ?= $(shell go list ./... | grep -v /vendor/) PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
VETPACKAGES ?= $(shell go list ./... | grep -v /vendor/ | grep -v /examples/)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*") GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
all: build all: install
install: deps install: deps
govendor sync govendor sync
.PHONY: test .PHONY: test
test: test:
go test -v -covermode=count -coverprofile=coverage.out sh coverage.sh
.PHONY: fmt .PHONY: fmt
fmt: fmt:
@ -26,7 +27,7 @@ fmt-check:
fi; fi;
vet: vet:
go vet $(PACKAGES) go vet $(VETPACKAGES)
deps: deps:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ @hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \

692
README.md
View File

@ -3,15 +3,124 @@
<img align="right" width="159px" src="https://raw.githubusercontent.com/gin-gonic/logo/master/color.png"> <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) [![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) [![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) [![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) [![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) [![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)
Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
![Gin console logger](https://gin-gonic.github.io/gin/other/console.png) ![Gin console logger](https://gin-gonic.github.io/gin/other/console.png)
## Contents
- [Installation](#installation)
- [Prerequisite](#prerequisite)
- [Quick start](#quick-start)
- [Benchmarks](#benchmarks)
- [Gin v1.stable](#gin-v1-stable)
- [Build with jsoniter](#build-with-jsoniter)
- [API Examples](#api-examples)
- [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
- [Parameters in path](#parameters-in-path)
- [Querystring parameters](#querystring-parameters)
- [Multipart/Urlencoded Form](#multiparturlencoded-form)
- [Another example: query + post form](#another-example-query--post-form)
- [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
- [Upload files](#upload-files)
- [Grouping routes](#grouping-routes)
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
- [Using middleware](#using-middleware)
- [How to write log file](#how-to-write-log-file)
- [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 HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
- [JSONP rendering](#jsonp)
- [Serving static files](#serving-static-files)
- [Serving data from reader](#serving-data-from-reader)
- [HTML rendering](#html-rendering)
- [Multitemplate](#multitemplate)
- [Redirects](#redirects)
- [Custom Middleware](#custom-middleware)
- [Using BasicAuth() middleware](#using-basicauth-middleware)
- [Goroutines inside a middleware](#goroutines-inside-a-middleware)
- [Custom HTTP configuration](#custom-http-configuration)
- [Support Let's Encrypt](#support-lets-encrypt)
- [Run multiple service using Gin](#run-multiple-service-using-gin)
- [Graceful restart or stop](#graceful-restart-or-stop)
- [Build a single binary with templates](#build-a-single-binary-with-templates)
- [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct)
- [Try to bind body into different structs](#try-to-bind-body-into-different-structs)
- [http2 server push](#http2-server-push)
- [Testing](#testing)
- [Users](#users)
## Installation
To install Gin package, you need to install Go and set your Go workspace first.
1. Download and install it:
```sh
$ go get -u github.com/gin-gonic/gin
```
2. Import it in your code:
```go
import "github.com/gin-gonic/gin"
```
3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
```go
import "net/http"
```
### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor)
1. `go get` govendor
```sh
$ go get github.com/kardianos/govendor
```
2. Create your project folder and `cd` inside
```sh
$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_"
```
3. Vendor init your project and add gin
```sh
$ govendor init
$ govendor fetch github.com/gin-gonic/gin@v1.3
```
4. Copy a starting template inside your project
```sh
$ curl https://raw.githubusercontent.com/gin-gonic/gin/master/examples/basic/main.go > main.go
```
5. Run your project
```sh
$ go run main.go
```
## Prerequisite
Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
## Quick start
```sh ```sh
# assume the following codes in example.go file # assume the following codes in example.go file
$ cat example.go $ cat example.go
@ -87,61 +196,9 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894
- [x] Battle tested - [x] Battle tested
- [x] API frozen, new releases will not break your code. - [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 ~/go/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) ## 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 ```sh
$ go build -tags=jsoniter . $ go build -tags=jsoniter .
@ -181,7 +238,7 @@ func main() {
func main() { func main() {
router := gin.Default() 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) { router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name") name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name) c.String(http.StatusOK, "Hello %s", name)
@ -268,6 +325,34 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great 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 ### Upload files
#### Single file #### Single file
@ -385,7 +470,7 @@ func main() {
r := gin.New() r := gin.New()
// Global middleware // Global middleware
// Logger middleware will write the logs to gin.DefaultWriter even you set with GIN_MODE=release. // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
// By default gin.DefaultWriter = os.Stdout // By default gin.DefaultWriter = os.Stdout
r.Use(gin.Logger()) r.Use(gin.Logger())
@ -435,7 +520,7 @@ func main() {
c.String(200, "pong") c.String(200, "pong")
}) })
r.Run(":8080")    router.Run(":8080")
} }
``` ```
@ -449,10 +534,10 @@ Note that you need to set the corresponding binding tag on all fields you want t
Also, Gin provides two sets of methods for binding: Also, Gin provides two sets of methods for binding:
- **Type** - Must bind - **Type** - Must bind
- **Methods** - `Bind`, `BindJSON`, `BindQuery` - **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method. - **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
- **Type** - Should bind - **Type** - Should bind
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindQuery` - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately. - **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`. When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
@ -462,8 +547,8 @@ You can also specify that specific fields are required. If a field is decorated
```go ```go
// Binding from JSON // Binding from JSON
type Login struct { type Login struct {
User string `form:"user" json:"user" binding:"required"` User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"` Password string `form:"password" json:"password" xml:"password" binding:"required"`
} }
func main() { func main() {
@ -472,30 +557,55 @@ func main() {
// Example for binding JSON ({"user": "manu", "password": "123"}) // Example for binding JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) { router.POST("/loginJSON", func(c *gin.Context) {
var json Login var json Login
if err = c.ShouldBindJSON(&json); err == nil { if err := c.ShouldBindXML(&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 {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 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</user>
// </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) // Example for binding a HTML form (user=manu&password=123)
router.POST("/loginForm", func(c *gin.Context) { router.POST("/loginForm", func(c *gin.Context) {
var form Login var form Login
// This will infer what binder to use depending on the content-type header. // This will infer what binder to use depending on the content-type header.
if err := c.ShouldBind(&form); err == nil { 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 {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 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 // Listen and serve on 0.0.0.0:8080
@ -525,6 +635,10 @@ $ curl -v -X POST \
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"} {"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
``` ```
**Skip validate**
When running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again.
### Custom Validators ### Custom Validators
It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go). It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go).
@ -540,7 +654,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
validator "gopkg.in/go-playground/validator.v8" "gopkg.in/go-playground/validator.v8"
) )
type Booking struct { type Booking struct {
@ -563,7 +677,11 @@ func bookableDate(
func main() { func main() {
route := gin.Default() route := gin.Default()
binding.Validator.RegisterValidation("bookabledate", bookableDate)
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable) route.GET("/bookable", getBookable)
route.Run(":8085") route.Run(":8085")
} }
@ -579,13 +697,16 @@ func getBookable(c *gin.Context) {
``` ```
```console ```console
$ curl "localhost:8085/bookable?check_in=2017-08-16&check_out=2017-08-17" $ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
{"message":"Booking dates are valid!"} {"message":"Booking dates are valid!"}
$ curl "localhost:8085/bookable?check_in=2017-08-15&check_out=2017-08-16" $ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"} {"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}
``` ```
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
See the [struct-lvl-validation example](examples/struct-lvl-validations) to learn more.
### Only Bind Query String ### Only Bind Query String
`ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017). `ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017).
@ -750,7 +871,7 @@ Test it with:
$ curl -v --form user=user --form password=password http://localhost:8080/login $ curl -v --form user=user --form password=password http://localhost:8080/login
``` ```
### XML, JSON and YAML rendering ### XML, JSON, YAML and ProtoBuf rendering
```go ```go
func main() { func main() {
@ -784,6 +905,19 @@ func main() {
c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) 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 // Listen and serve on 0.0.0.0:8080
r.Run(":8080") r.Run(":8080")
} }
@ -811,6 +945,51 @@ func main() {
r.Run(":8080") r.Run(":8080")
} }
``` ```
#### JSONP
Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists.
```go
func main() {
r := gin.Default()
r.GET("/JSONP?callback=x", func(c *gin.Context) {
data := map[string]interface{}{
"foo": "bar",
}
//callback is x
// Will output : x({\"foo\":\"bar\"})
c.JSONP(http.StatusOK, data)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
```
#### AsciiJSON
Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters.
```go
func main() {
r := gin.Default()
r.GET("/someJSON", func(c *gin.Context) {
data := map[string]interface{}{
"lang": "GO语言",
"tag": "<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 #### PureJSON
@ -854,6 +1033,32 @@ func main() {
} }
``` ```
### Serving data from reader
```go
func main() {
router := gin.Default()
router.GET("/someDataFromReader", func(c *gin.Context) {
response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png")
if err != nil || response.StatusCode != http.StatusOK {
c.Status(http.StatusServiceUnavailable)
return
}
reader := response.Body
contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type")
extraHeaders := map[string]string{
"Content-Disposition": `attachment; filename="gopher.png"`,
}
c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders)
})
router.Run(":8080")
}
```
### HTML rendering ### HTML rendering
Using LoadHTMLGlob() or LoadHTMLFiles() Using LoadHTMLGlob() or LoadHTMLFiles()
@ -978,7 +1183,7 @@ func main() {
router.SetFuncMap(template.FuncMap{ router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate, "formatAsDate": formatAsDate,
}) })
router.LoadHTMLFiles("./fixtures/basic/raw.tmpl") router.LoadHTMLFiles("./testdata/template/raw.tmpl")
router.GET("/raw", func(c *gin.Context) { router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
@ -1008,14 +1213,26 @@ Gin allow by default use only one html.Template. Check [a multitemplate render](
### Redirects ### Redirects
Issuing a HTTP redirect is easy: Issuing a HTTP redirect is easy. Both internal and external locations are supported.
```go ```go
r.GET("/test", func(c *gin.Context) { r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/") c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
}) })
``` ```
Both internal and external locations are supported.
Issuing a Router redirect, use `HandleContext` like below.
``` go
r.GET("/test", func(c *gin.Context) {
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(200, gin.H{"hello": "world"})
})
```
### Custom Middleware ### Custom Middleware
@ -1099,7 +1316,7 @@ func main() {
### Goroutines inside a middleware ### Goroutines inside a middleware
When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. When starting new Goroutines inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy.
```go ```go
func main() { func main() {
@ -1161,7 +1378,7 @@ func main() {
example for 1-line LetsEncrypt HTTPS servers. example for 1-line LetsEncrypt HTTPS servers.
[embedmd]:# (examples/auto-tls/example1.go go) [embedmd]:# (examples/auto-tls/example1/main.go go)
```go ```go
package main package main
@ -1186,7 +1403,7 @@ func main() {
example for custom autocert manager. example for custom autocert manager.
[embedmd]:# (examples/auto-tls/example2.go go) [embedmd]:# (examples/auto-tls/example2/main.go go)
```go ```go
package main package main
@ -1218,7 +1435,7 @@ func main() {
### Run multiple service using Gin ### Run multiple service using Gin
See the [question](https://github.com/gin-gonic/gin/issues/346) and try the folling example: See the [question](https://github.com/gin-gonic/gin/issues/346) and try the following example:
[embedmd]:# (examples/multiple-service/main.go go) [embedmd]:# (examples/multiple-service/main.go go)
```go ```go
@ -1351,8 +1568,8 @@ func main() {
go func() { go func() {
// service connections // service connections
if err := srv.ListenAndServe(); err != nil { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("listen: %s\n", err) log.Fatalf("listen: %s\n", err)
} }
}() }()
@ -1372,7 +1589,296 @@ func main() {
} }
``` ```
## Users [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) ### Build a single binary with templates
You can build a server into a single binary containing templates by using [go-assets][].
[go-assets]: https://github.com/jessevdk/go-assets
```go
func main() {
r := gin.New()
t, err := loadTemplate()
if err != nil {
panic(err)
}
r.SetHTMLTemplate(t)
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "/html/index.tmpl",nil)
})
r.Run(":8080")
}
// loadTemplate loads templates embedded by go-assets-builder
func loadTemplate() (*template.Template, error) {
t := template.New("")
for name, file := range Assets.Files {
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
continue
}
h, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
t, err = t.New(name).Parse(string(h))
if err != nil {
return nil, err
}
}
return t, nil
}
```
See a complete example in the `examples/assets-in-binary` directory.
### Bind form-data request with custom struct
The follow example using custom struct:
```go
type StructA struct {
FieldA string `form:"field_a"`
}
type StructB struct {
NestedStruct StructA
FieldB string `form:"field_b"`
}
type StructC struct {
NestedStructPointer *StructA
FieldC string `form:"field_c"`
}
type StructD struct {
NestedAnonyStruct struct {
FieldX string `form:"field_x"`
}
FieldD string `form:"field_d"`
}
func GetDataB(c *gin.Context) {
var b StructB
c.Bind(&b)
c.JSON(200, gin.H{
"a": b.NestedStruct,
"b": b.FieldB,
})
}
func GetDataC(c *gin.Context) {
var b StructC
c.Bind(&b)
c.JSON(200, gin.H{
"a": b.NestedStructPointer,
"c": b.FieldC,
})
}
func GetDataD(c *gin.Context) {
var b StructD
c.Bind(&b)
c.JSON(200, gin.H{
"x": b.NestedAnonyStruct,
"d": b.FieldD,
})
}
func main() {
r := gin.Default()
r.GET("/getb", GetDataB)
r.GET("/getc", GetDataC)
r.GET("/getd", GetDataD)
r.Run()
}
```
Using the command `curl` command result:
```
$ curl "http://localhost:8080/getb?field_a=hello&field_b=world"
{"a":{"FieldA":"hello"},"b":"world"}
$ curl "http://localhost:8080/getc?field_a=hello&field_c=world"
{"a":{"FieldA":"hello"},"c":"world"}
$ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
{"d":"world","x":{"FieldX":"hello"}}
```
**NOTE**: NOT support the follow style struct:
```go
type StructX struct {
X struct {} `form:"name_x"` // HERE have form
}
type StructY struct {
Y StructX `form:"name_y"` // HERE hava form
}
type StructZ struct {
Z *StructZ `form:"name_z"` // HERE hava form
}
```
In a word, only support nested custom struct which have no `form` now.
### Try to bind body into different structs
The normal methods for binding request body consumes `c.Request.Body` and they
cannot be called multiple times.
```go
type formA struct {
Foo string `json:"foo" xml:"foo" binding:"required"`
}
type formB struct {
Bar string `json:"bar" xml:"bar" binding:"required"`
}
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This c.ShouldBind consumes c.Request.Body and it cannot be reused.
if errA := c.ShouldBind(&objA); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// Always an error is occurred by this because c.Request.Body is EOF now.
} else if errB := c.ShouldBind(&objB); errB == nil {
c.String(http.StatusOK, `the body should be formB`)
} else {
...
}
}
```
For this, you can use `c.ShouldBindBodyWith`.
```go
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// This reads c.Request.Body and stores the result into the context.
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// At this time, it reuses body stored in the context.
} else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`)
// And it can accepts other formats
} else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`)
} else {
...
}
}
```
* `c.ShouldBindBodyWith` stores body into the context before binding. This has
a slight impact to performance, so you should not use this method if you are
enough to call binding at once.
* This feature is only needed for some formats -- `JSON`, `XML`, `MsgPack`,
`ProtoBuf`. For other formats, `Query`, `Form`, `FormPost`, `FormMultipart`,
can be called by `c.ShouldBind()` multiple times without any damage to
performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)).
### http2 server push
http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information.
[embedmd]:# (examples/http-pusher/main.go go)
```go
package main
import (
"html/template"
"log"
"github.com/gin-gonic/gin"
)
var html = template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
<script src="/assets/app.js"></script>
</head>
<body>
<h1 style="color:red;">Welcome, Ginner!</h1>
</body>
</html>
`))
func main() {
r := gin.Default()
r.Static("/assets", "./assets")
r.SetHTMLTemplate(html)
r.GET("/", func(c *gin.Context) {
if pusher := c.Writer.Pusher(); pusher != nil {
// use pusher.Push() to do server push
if err := pusher.Push("/assets/app.js", nil); err != nil {
log.Printf("Failed to push: %v", err)
}
}
c.HTML(200, "https", gin.H{
"status": "success",
})
})
// Listen and Server in https://127.0.0.1:8080
r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key")
}
```
## Testing
The `net/http/httptest` package is preferable way for HTTP testing.
```go
package main
func setupRouter() *gin.Engine {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
return r
}
func main() {
r := setupRouter()
r.Run(":8080")
}
```
Test for code example above:
```go
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPingRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, "pong", w.Body.String())
}
```
## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.

15
auth.go
View File

@ -7,6 +7,7 @@ package gin
import ( import (
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"net/http"
"strconv" "strconv"
) )
@ -17,8 +18,8 @@ const AuthUserKey = "user"
type Accounts map[string]string type Accounts map[string]string
type authPair struct { type authPair struct {
Value string value string
User string user string
} }
type authPairs []authPair type authPairs []authPair
@ -28,8 +29,8 @@ func (a authPairs) searchCredential(authValue string) (string, bool) {
return "", false return "", false
} }
for _, pair := range a { for _, pair := range a {
if pair.Value == authValue { if pair.value == authValue {
return pair.User, true return pair.user, true
} }
} }
return "", false return "", false
@ -51,7 +52,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
if !found { if !found {
// Credentials doesn't match, we return 401 and abort handlers chain. // Credentials doesn't match, we return 401 and abort handlers chain.
c.Header("WWW-Authenticate", realm) c.Header("WWW-Authenticate", realm)
c.AbortWithStatus(401) c.AbortWithStatus(http.StatusUnauthorized)
return return
} }
@ -74,8 +75,8 @@ func processAccounts(accounts Accounts) authPairs {
assert1(user != "", "User can not be empty") assert1(user != "", "User can not be empty")
value := authorizationHeader(user, password) value := authorizationHeader(user, password)
pairs = append(pairs, authPair{ pairs = append(pairs, authPair{
Value: value, value: value,
User: user, user: user,
}) })
} }
return pairs return pairs

View File

@ -22,16 +22,16 @@ func TestBasicAuth(t *testing.T) {
assert.Len(t, pairs, 3) assert.Len(t, pairs, 3)
assert.Contains(t, pairs, authPair{ assert.Contains(t, pairs, authPair{
User: "bar", user: "bar",
Value: "Basic YmFyOmZvbw==", value: "Basic YmFyOmZvbw==",
}) })
assert.Contains(t, pairs, authPair{ assert.Contains(t, pairs, authPair{
User: "foo", user: "foo",
Value: "Basic Zm9vOmJhcg==", value: "Basic Zm9vOmJhcg==",
}) })
assert.Contains(t, pairs, authPair{ assert.Contains(t, pairs, authPair{
User: "admin", user: "admin",
Value: "Basic YWRtaW46cGFzc3dvcmQ=", value: "Basic YWRtaW46cGFzc3dvcmQ=",
}) })
} }
@ -93,7 +93,7 @@ func TestBasicAuthSucceed(t *testing.T) {
router := New() router := New()
router.Use(BasicAuth(accounts)) router.Use(BasicAuth(accounts))
router.GET("/login", func(c *Context) { router.GET("/login", func(c *Context) {
c.String(200, c.MustGet(AuthUserKey).(string)) c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -101,7 +101,7 @@ func TestBasicAuthSucceed(t *testing.T) {
req.Header.Set("Authorization", authorizationHeader("admin", "password")) req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "admin", w.Body.String()) assert.Equal(t, "admin", w.Body.String())
} }
@ -112,7 +112,7 @@ func TestBasicAuth401(t *testing.T) {
router.Use(BasicAuth(accounts)) router.Use(BasicAuth(accounts))
router.GET("/login", func(c *Context) { router.GET("/login", func(c *Context) {
called = true called = true
c.String(200, c.MustGet(AuthUserKey).(string)) c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -121,7 +121,7 @@ func TestBasicAuth401(t *testing.T) {
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.False(t, called) assert.False(t, called)
assert.Equal(t, 401, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "Basic realm=\"Authorization Required\"", w.HeaderMap.Get("WWW-Authenticate")) assert.Equal(t, "Basic realm=\"Authorization Required\"", w.HeaderMap.Get("WWW-Authenticate"))
} }
@ -132,7 +132,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\"")) router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\""))
router.GET("/login", func(c *Context) { router.GET("/login", func(c *Context) {
called = true called = true
c.String(200, c.MustGet(AuthUserKey).(string)) c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
}) })
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -141,6 +141,6 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.False(t, called) assert.False(t, called)
assert.Equal(t, 401, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.HeaderMap.Get("WWW-Authenticate")) assert.Equal(t, "Basic realm=\"My Custom \\\"Realm\\\"\"", w.HeaderMap.Get("WWW-Authenticate"))
} }

View File

@ -54,13 +54,11 @@ func BenchmarkOneRouteJSON(B *testing.B) {
Status string `json:"status"` Status string `json:"status"`
}{"ok"} }{"ok"}
router.GET("/json", func(c *Context) { router.GET("/json", func(c *Context) {
c.JSON(200, data) c.JSON(http.StatusOK, data)
}) })
runRequest(B, router, "GET", "/json") runRequest(B, router, "GET", "/json")
} }
var htmlContentType = []string{"text/html; charset=utf-8"}
func BenchmarkOneRouteHTML(B *testing.B) { func BenchmarkOneRouteHTML(B *testing.B) {
router := New() router := New()
t := template.Must(template.New("index").Parse(` t := template.Must(template.New("index").Parse(`
@ -68,7 +66,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
router.SetHTMLTemplate(t) router.SetHTMLTemplate(t)
router.GET("/html", func(c *Context) { router.GET("/html", func(c *Context) {
c.HTML(200, "index", "hola") c.HTML(http.StatusOK, "index", "hola")
}) })
runRequest(B, router, "GET", "/html") runRequest(B, router, "GET", "/html")
} }
@ -84,7 +82,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
func BenchmarkOneRouteString(B *testing.B) { func BenchmarkOneRouteString(B *testing.B) {
router := New() router := New()
router.GET("/text", func(c *Context) { 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") runRequest(B, router, "GET", "/text")
} }

View File

@ -4,12 +4,9 @@
package binding package binding
import ( import "net/http"
"net/http"
"gopkg.in/go-playground/validator.v8"
)
// Content-Type MIME of the most common data formats.
const ( const (
MIMEJSON = "application/json" MIMEJSON = "application/json"
MIMEHTML = "text/html" MIMEHTML = "text/html"
@ -23,11 +20,25 @@ const (
MIMEMSGPACK2 = "application/msgpack" MIMEMSGPACK2 = "application/msgpack"
) )
// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface { type Binding interface {
Name() string Name() string
Bind(*http.Request, interface{}) error Bind(*http.Request, interface{}) error
} }
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
Binding
BindBody([]byte, interface{}) error
}
// 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
// https://github.com/go-playground/validator/tree/v8.18.2.
type StructValidator interface { type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned. // If the received type is not a struct, any validation should be skipped and nil must be returned.
@ -36,14 +47,18 @@ type StructValidator interface {
// Otherwise nil must be returned. // Otherwise nil must be returned.
ValidateStruct(interface{}) error ValidateStruct(interface{}) error
// RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key // Engine returns the underlying validator engine which powers the
// NOTE: if the key already exists, the previous validation function will be replaced. // StructValidator implementation.
// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation Engine() interface{}
RegisterValidation(string, validator.Func) error
} }
// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
// under the hood.
var Validator StructValidator = &defaultValidator{} var Validator StructValidator = &defaultValidator{}
// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var ( var (
JSON = jsonBinding{} JSON = jsonBinding{}
XML = xmlBinding{} XML = xmlBinding{}
@ -55,6 +70,8 @@ var (
MsgPack = msgpackBinding{} MsgPack = msgpackBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
func Default(method, contentType string) Binding { func Default(method, contentType string) Binding {
if method == "GET" { if method == "GET" {
return Form return Form

View File

@ -0,0 +1,67 @@
package binding
import (
"bytes"
"io/ioutil"
"testing"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
func TestBindingBody(t *testing.T) {
for _, tt := range []struct {
name string
binding BindingBody
body string
want string
}{
{
name: "JSON bidning",
binding: JSON,
body: `{"foo":"FOO"}`,
},
{
name: "XML bidning",
binding: XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: "MsgPack binding",
binding: MsgPack,
body: msgPackBody(t),
},
} {
t.Logf("testing: %s", tt.name)
req := requestWithBody("POST", "/", tt.body)
form := FooStruct{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, tt.binding.BindBody(body, &form))
assert.Equal(t, FooStruct{"FOO"}, form)
}
}
func msgPackBody(t *testing.T) string {
test := FooStruct{"FOO"}
h := new(codec.MsgpackHandle)
buf := bytes.NewBuffer(nil)
assert.NoError(t, codec.NewEncoder(buf, h).Encode(test))
return buf.String()
}
func TestBindingBodyProto(t *testing.T) {
test := protoexample.Test{
Label: proto.String("FOO"),
}
data, _ := proto.Marshal(&test)
req := requestWithBody("POST", "/", string(data))
form := protoexample.Test{}
body, _ := ioutil.ReadAll(req.Body)
assert.NoError(t, ProtoBuf.BindBody(body, &form))
assert.Equal(t, test, form)
}

File diff suppressed because it is too large Load Diff

View File

@ -18,19 +18,29 @@ type defaultValidator struct {
var _ StructValidator = &defaultValidator{} 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 { 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() v.lazyinit()
if err := v.validate.Struct(obj); err != nil { if err := v.validate.Struct(obj); err != nil {
return error(err) return err
} }
} }
return nil return nil
} }
func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) error { // Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info -
// https://godoc.org/gopkg.in/go-playground/validator.v8
func (v *defaultValidator) Engine() interface{} {
v.lazyinit() v.lazyinit()
return v.validate.RegisterValidation(key, fn) return v.validate
} }
func (v *defaultValidator) lazyinit() { func (v *defaultValidator) lazyinit() {
@ -39,12 +49,3 @@ func (v *defaultValidator) lazyinit() {
v.validate = validator.New(config) 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

@ -8,6 +8,7 @@ import (
"errors" "errors"
"reflect" "reflect"
"strconv" "strconv"
"strings"
"time" "time"
) )
@ -23,12 +24,28 @@ func mapForm(ptr interface{}, form map[string][]string) error {
structFieldKind := structField.Kind() structFieldKind := structField.Kind()
inputFieldName := typeField.Tag.Get("form") inputFieldName := typeField.Tag.Get("form")
inputFieldNameList := strings.Split(inputFieldName, ",")
inputFieldName = inputFieldNameList[0]
var defaultValue string
if len(inputFieldNameList) > 1 {
defaultList := strings.SplitN(inputFieldNameList[1], "=", 2)
if defaultList[0] == "default" {
defaultValue = defaultList[1]
}
}
if inputFieldName == "" { if inputFieldName == "" {
inputFieldName = typeField.Name inputFieldName = typeField.Name
// if "form" tag is nil, we inspect if the field is a struct. // if "form" tag is nil, we inspect if the field is a struct or struct pointer.
// this would not make sense for JSON parsing but it does for a form // this would not make sense for JSON parsing but it does for a form
// since data is flatten // since data is flatten
if structFieldKind == reflect.Ptr {
if !structField.Elem().IsValid() {
structField.Set(reflect.New(structField.Type().Elem()))
}
structField = structField.Elem()
structFieldKind = structField.Kind()
}
if structFieldKind == reflect.Struct { if structFieldKind == reflect.Struct {
err := mapForm(structField.Addr().Interface(), form) err := mapForm(structField.Addr().Interface(), form)
if err != nil { if err != nil {
@ -38,8 +55,13 @@ func mapForm(ptr interface{}, form map[string][]string) error {
} }
} }
inputValue, exists := form[inputFieldName] inputValue, exists := form[inputFieldName]
if !exists { if !exists {
continue if defaultValue == "" {
continue
}
inputValue = make([]string, 1)
inputValue[0] = defaultValue
} }
numElems := len(inputValue) numElems := len(inputValue)
@ -97,6 +119,12 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V
return setFloatField(val, 64, structField) return setFloatField(val, 64, structField)
case reflect.String: case reflect.String:
structField.SetString(val) structField.SetString(val)
case reflect.Ptr:
if !structField.Elem().IsValid() {
structField.Set(reflect.New(structField.Type().Elem()))
}
structFieldElem := structField.Elem()
return setWithProperType(structFieldElem.Kind(), val, structFieldElem)
default: default:
return errors.New("Unknown type") return errors.New("Unknown type")
} }
@ -133,7 +161,7 @@ func setBoolField(val string, field reflect.Value) error {
if err == nil { if err == nil {
field.SetBool(boolVal) field.SetBool(boolVal)
} }
return nil return err
} }
func setFloatField(val string, bitSize int, field reflect.Value) error { func setFloatField(val string, bitSize int, field reflect.Value) error {
@ -150,7 +178,7 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
func setTimeField(val string, structField reflect.StructField, value reflect.Value) error { func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
timeFormat := structField.Tag.Get("time_format") timeFormat := structField.Tag.Get("time_format")
if timeFormat == "" { if timeFormat == "" {
return errors.New("Blank time format") timeFormat = time.RFC3339
} }
if val == "" { if val == "" {
@ -179,12 +207,3 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
value.Set(reflect.ValueOf(t)) value.Set(reflect.ValueOf(t))
return nil return nil
} }
// Don't pass in pointers to bind to. Can lead to bugs. See:
// https://github.com/codegangsta/martini-contrib/issues/40
// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
func ensureNotPointer(obj interface{}) {
if reflect.TypeOf(obj).Kind() == reflect.Ptr {
panic("Pointers are not accepted as binding models")
}
}

View File

@ -5,11 +5,16 @@
package binding package binding
import ( import (
"bytes"
"io"
"net/http" "net/http"
"github.com/gin-gonic/gin/json" "github.com/gin-gonic/gin/json"
) )
// EnableDecoderUseNumber is used to call the UseNumber method on the JSON
// Decoder instance. UseNumber causes the Decoder to unmarshal a number into an
// interface{} as a Number instead of as a float64.
var EnableDecoderUseNumber = false var EnableDecoderUseNumber = false
type jsonBinding struct{} type jsonBinding struct{}
@ -19,7 +24,15 @@ func (jsonBinding) Name() string {
} }
func (jsonBinding) Bind(req *http.Request, obj interface{}) error { func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
decoder := json.NewDecoder(req.Body) return decodeJSON(req.Body, obj)
}
func (jsonBinding) BindBody(body []byte, obj interface{}) error {
return decodeJSON(bytes.NewReader(body), obj)
}
func decodeJSON(r io.Reader, obj interface{}) error {
decoder := json.NewDecoder(r)
if EnableDecoderUseNumber { if EnableDecoderUseNumber {
decoder.UseNumber() decoder.UseNumber()
} }

View File

@ -5,6 +5,8 @@
package binding package binding
import ( import (
"bytes"
"io"
"net/http" "net/http"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
@ -17,7 +19,16 @@ func (msgpackBinding) Name() string {
} }
func (msgpackBinding) Bind(req *http.Request, obj interface{}) error { func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
if err := codec.NewDecoder(req.Body, new(codec.MsgpackHandle)).Decode(&obj); err != nil { return decodeMsgPack(req.Body, obj)
}
func (msgpackBinding) BindBody(body []byte, obj interface{}) error {
return decodeMsgPack(bytes.NewReader(body), obj)
}
func decodeMsgPack(r io.Reader, obj interface{}) error {
cdc := new(codec.MsgpackHandle)
if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
return err return err
} }
return validate(obj) return validate(obj)

View File

@ -17,19 +17,20 @@ func (protobufBinding) Name() string {
return "protobuf" return "protobuf"
} }
func (protobufBinding) Bind(req *http.Request, obj interface{}) error { func (b protobufBinding) Bind(req *http.Request, obj interface{}) error {
buf, err := ioutil.ReadAll(req.Body) buf, err := ioutil.ReadAll(req.Body)
if err != nil { if err != nil {
return err return err
} }
return b.BindBody(buf, obj)
}
if err = proto.Unmarshal(buf, obj.(proto.Message)); err != nil { func (protobufBinding) BindBody(body []byte, obj interface{}) error {
if err := proto.Unmarshal(body, obj.(proto.Message)); err != nil {
return err 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 cann't add `binding:""` to the struct // `binding:""` to the struct which automatically generate by gen-proto
//which automatically generate by gen-proto
return nil return nil
//return validate(obj) // return validate(obj)
} }

View File

@ -176,7 +176,7 @@ func TestValidatePrimitives(t *testing.T) {
obj := Object{"foo": "bar", "bar": 1} obj := Object{"foo": "bar", "bar": 1}
assert.NoError(t, validate(obj)) assert.NoError(t, validate(obj))
assert.NoError(t, validate(&obj)) assert.NoError(t, validate(&obj))
assert.Equal(t, obj, Object{"foo": "bar", "bar": 1}) assert.Equal(t, Object{"foo": "bar", "bar": 1}, obj)
obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}} obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}}
assert.NoError(t, validate(obj2)) assert.NoError(t, validate(obj2))
@ -185,12 +185,12 @@ func TestValidatePrimitives(t *testing.T) {
nu := 10 nu := 10
assert.NoError(t, validate(nu)) assert.NoError(t, validate(nu))
assert.NoError(t, validate(&nu)) assert.NoError(t, validate(&nu))
assert.Equal(t, nu, 10) assert.Equal(t, 10, nu)
str := "value" str := "value"
assert.NoError(t, validate(str)) assert.NoError(t, validate(str))
assert.NoError(t, validate(&str)) assert.NoError(t, validate(&str))
assert.Equal(t, str, "value") assert.Equal(t, "value", str)
} }
// structCustomValidation is a helper struct we use to check that // structCustomValidation is a helper struct we use to check that
@ -214,11 +214,14 @@ func notOne(
return false return false
} }
func TestRegisterValidation(t *testing.T) { func TestValidatorEngine(t *testing.T) {
// This validates that the function `notOne` matches // This validates that the function `notOne` matches
// the expected function signature by `defaultValidator` // the expected function signature by `defaultValidator`
// and by extension the validator library. // and by extension the validator library.
err := Validator.RegisterValidation("notone", notOne) engine, ok := Validator.Engine().(*validator.Validate)
assert.True(t, ok)
err := engine.RegisterValidation("notone", notOne)
// Check that we can register custom validation without error // Check that we can register custom validation without error
assert.Nil(t, err) assert.Nil(t, err)
@ -228,6 +231,6 @@ func TestRegisterValidation(t *testing.T) {
// Check that we got back non-nil errs // Check that we got back non-nil errs
assert.NotNil(t, errs) assert.NotNil(t, errs)
// Check that the error matches expactation // Check that the error matches expectation
assert.Error(t, errs, "", "", "notone") assert.Error(t, errs, "", "", "notone")
} }

View File

@ -5,7 +5,9 @@
package binding package binding
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"io"
"net/http" "net/http"
) )
@ -16,7 +18,14 @@ func (xmlBinding) Name() string {
} }
func (xmlBinding) Bind(req *http.Request, obj interface{}) error { func (xmlBinding) Bind(req *http.Request, obj interface{}) error {
decoder := xml.NewDecoder(req.Body) return decodeXML(req.Body, obj)
}
func (xmlBinding) BindBody(body []byte, obj interface{}) error {
return decodeXML(bytes.NewReader(body), obj)
}
func decodeXML(r io.Reader, obj interface{}) error {
decoder := xml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil { if err := decoder.Decode(obj); err != nil {
return err return err
} }

View File

@ -31,11 +31,10 @@ const (
MIMEPlain = binding.MIMEPlain MIMEPlain = binding.MIMEPlain
MIMEPOSTForm = binding.MIMEPOSTForm MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
) )
const ( const abortIndex int8 = math.MaxInt8 / 2
abortIndex int8 = math.MaxInt8 / 2
)
// Context is the most important part of gin. It allows us to pass variables between middleware, // Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example. // manage the flow, validate the JSON of a request and render a JSON response for example.
@ -105,8 +104,7 @@ func (c *Context) Handler() HandlerFunc {
// See example in GitHub. // See example in GitHub.
func (c *Context) Next() { func (c *Context) Next() {
c.index++ c.index++
s := int8(len(c.handlers)) for s := int8(len(c.handlers)); c.index < s; c.index++ {
for ; c.index < s; c.index++ {
c.handlers[c.index](c) c.handlers[c.index](c)
} }
} }
@ -161,16 +159,15 @@ func (c *Context) Error(err error) *Error {
if err == nil { if err == nil {
panic("err is nil") panic("err is nil")
} }
var parsedError *Error
switch err.(type) { parsedError, ok := err.(*Error)
case *Error: if !ok {
parsedError = err.(*Error)
default:
parsedError = &Error{ parsedError = &Error{
Err: err, Err: err,
Type: ErrorTypePrivate, Type: ErrorTypePrivate,
} }
} }
c.Errors = append(c.Errors, parsedError) c.Errors = append(c.Errors, parsedError)
return parsedError return parsedError
} }
@ -363,6 +360,18 @@ func (c *Context) GetQueryArray(key string) ([]string, bool) {
return []string{}, false 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 // PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`. // when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) string { func (c *Context) PostForm(key string) string {
@ -418,6 +427,42 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false return []string{}, false
} }
// PostFormMap returns a map for a given form key.
func (c *Context) PostFormMap(key string) map[string]string {
dicts, _ := c.GetPostFormMap(key)
return dicts
}
// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
req := c.Request
req.ParseForm()
req.ParseMultipartForm(c.engine.MaxMultipartMemory)
dicts, exist := c.get(req.PostForm, key)
if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
dicts, exist = c.get(req.MultipartForm.Value, key)
}
return dicts, exist
}
// get is an internal method and returns a map which satisfy conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
dicts := make(map[string]string)
exist := false
for k, v := range m {
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
exist = true
dicts[k[i+1:][:j]] = v[0]
}
}
}
return dicts, exist
}
// FormFile returns the first file for the provided form key. // FormFile returns the first file for the provided form key.
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) { func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
_, fh, err := c.Request.FormFile(name) _, fh, err := c.Request.FormFile(name)
@ -452,10 +497,10 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string) error
// Depending the "Content-Type" header different bindings are used: // Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding // "application/json" --> JSON binding
// "application/xml" --> XML binding // "application/xml" --> XML binding
// otherwise --> returns an error // otherwise --> returns an error.
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. // It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
// It decodes the json payload into the struct specified as a pointer. // It decodes the json payload into the struct specified as a pointer.
// It will writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. // It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
func (c *Context) Bind(obj interface{}) error { func (c *Context) Bind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType()) b := binding.Default(c.Request.Method, c.ContentType())
return c.MustBindWith(obj, b) return c.MustBindWith(obj, b)
@ -466,6 +511,11 @@ func (c *Context) BindJSON(obj interface{}) error {
return c.MustBindWith(obj, binding.JSON) 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). // BindQuery is a shortcut for c.MustBindWith(obj, binding.Query).
func (c *Context) BindQuery(obj interface{}) error { func (c *Context) BindQuery(obj interface{}) error {
return c.MustBindWith(obj, binding.Query) return c.MustBindWith(obj, binding.Query)
@ -476,7 +526,7 @@ func (c *Context) BindQuery(obj interface{}) error {
// See the binding package. // See the binding package.
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) { func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
if err = c.ShouldBindWith(obj, b); err != nil { if err = c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(400, err).SetType(ErrorTypeBind) c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
} }
return return
@ -500,6 +550,11 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
return c.ShouldBindWith(obj, binding.JSON) 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). // ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error { func (c *Context) ShouldBindQuery(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Query) return c.ShouldBindWith(obj, binding.Query)
@ -511,21 +566,41 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj) return b.Bind(c.Request, obj)
} }
// ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request
// body into the context, and reuse when it is called again.
//
// NOTE: This method reads the body before binding. So you should use
// ShouldBindWith for better performance if you need to call only once.
func (c *Context) ShouldBindBodyWith(
obj interface{}, bb binding.BindingBody,
) (err error) {
var body []byte
if cb, ok := c.Get(BodyBytesKey); ok {
if cbb, ok := cb.([]byte); ok {
body = cbb
}
}
if body == nil {
body, err = ioutil.ReadAll(c.Request.Body)
if err != nil {
return err
}
c.Set(BodyBytesKey, body)
}
return bb.BindBody(body, obj)
}
// ClientIP implements a best effort algorithm to return the real client IP, it parses // ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP. // Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP { if c.engine.ForwardedByClientIP {
clientIP := c.requestHeader("X-Forwarded-For") clientIP := c.requestHeader("X-Forwarded-For")
if index := strings.IndexByte(clientIP, ','); index >= 0 { clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
clientIP = clientIP[0:index] if clientIP == "" {
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
} }
clientIP = strings.TrimSpace(clientIP) if clientIP != "" {
if len(clientIP) > 0 {
return clientIP
}
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
if len(clientIP) > 0 {
return clientIP return clientIP
} }
} }
@ -571,9 +646,9 @@ func bodyAllowedForStatus(status int) bool {
switch { switch {
case status >= 100 && status <= 199: case status >= 100 && status <= 199:
return false return false
case status == 204: case status == http.StatusNoContent:
return false return false
case status == 304: case status == http.StatusNotModified:
return false return false
} }
return true return true
@ -588,7 +663,7 @@ func (c *Context) Status(code int) {
// It writes a header in the response. // It writes a header in the response.
// If value == "", this method removes the header `c.Writer.Header().Del(key)` // If value == "", this method removes the header `c.Writer.Header().Del(key)`
func (c *Context) Header(key, value string) { func (c *Context) Header(key, value string) {
if len(value) == 0 { if value == "" {
c.Writer.Header().Del(key) c.Writer.Header().Del(key)
} else { } else {
c.Writer.Header().Set(key, value) c.Writer.Header().Set(key, value)
@ -673,12 +748,30 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj}) c.Render(code, render.SecureJSON{Prefix: c.engine.secureJsonPrefix, Data: obj})
} }
// JSONP serializes the given struct as JSON into the response body.
// It add padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj interface{}) {
callback := c.DefaultQuery("callback", "")
if callback == "" {
c.Render(code, render.JSON{Data: obj})
} else {
c.Render(code, render.JsonpJSON{Callback: callback, Data: obj})
}
}
// JSON serializes the given struct as JSON into the response body. // JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) { func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj}) 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. // XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml". // It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) { func (c *Context) XML(code int, obj interface{}) {
@ -690,6 +783,11 @@ func (c *Context) YAML(code int, obj interface{}) {
c.Render(code, render.YAML{Data: obj}) 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. // String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) { func (c *Context) String(code int, format string, values ...interface{}) {
c.Render(code, render.String{Format: format, Data: values}) c.Render(code, render.String{Format: format, Data: values})
@ -712,6 +810,16 @@ func (c *Context) Data(code int, contentType string, data []byte) {
}) })
} }
// DataFromReader writes the specified reader into the body stream and updates the HTTP code.
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string) {
c.Render(code, render.Reader{
Headers: extraHeaders,
ContentType: contentType,
ContentLength: contentLength,
Reader: reader,
})
}
// File writes the specified file into the body stream in a efficient way. // File writes the specified file into the body stream in a efficient way.
func (c *Context) File(filepath string) { func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath) http.ServeFile(c.Writer, c.Request, filepath)
@ -801,18 +909,33 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/ /***** 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) { func (c *Context) Deadline() (deadline time.Time, ok bool) {
return 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{} { func (c *Context) Done() <-chan struct{} {
return nil 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 { func (c *Context) Err() error {
return nil 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{} { func (c *Context) Value(key interface{}) interface{} {
if key == 0 { if key == 0 {
return c.Request return c.Request

File diff suppressed because it is too large Load Diff

13
coverage.sh Normal file
View File

@ -0,0 +1,13 @@
#!/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

@ -15,7 +15,7 @@ func init() {
} }
// IsDebugging returns true if the framework is running in debug mode. // IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.Release) to disable debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode.
func IsDebugging() bool { func IsDebugging() bool {
return ginMode == debugCode return ginMode == debugCode
} }
@ -47,6 +47,9 @@ func debugPrint(format string, values ...interface{}) {
} }
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
debugPrint(`[WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.
`)
debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. debugPrint(`[WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
`) `)

View File

@ -72,7 +72,7 @@ func TestDebugPrintLoadTemplate(t *testing.T) {
setup(&w) setup(&w)
defer teardown() defer teardown()
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl")) templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./testdata/template/hello.tmpl"))
debugPrintLoadTemplate(templ) debugPrintLoadTemplate(templ)
assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, w.String()) assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, w.String())
} }
@ -92,7 +92,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
defer teardown() defer teardown()
debugPrintWARNINGDefault() debugPrintWARNINGDefault()
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", w.String()) assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", w.String())
} }
func TestDebugPrintWARNINGNew(t *testing.T) { func TestDebugPrintWARNINGNew(t *testing.T) {

31
deprecated_test.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin/binding"
"github.com/stretchr/testify/assert"
)
func TestBindWith(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused"))
var obj struct {
Foo string `form:"foo"`
Bar string `form:"bar"`
}
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

@ -148,7 +148,7 @@ func (a errorMsgs) String() string {
} }
var buffer bytes.Buffer var buffer bytes.Buffer
for i, msg := range a { for i, msg := range a {
fmt.Fprintf(&buffer, "Error #%02d: %s\n", (i + 1), msg.Err) fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
if msg.Meta != nil { if msg.Meta != nil {
fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta) fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta)
} }

View File

@ -19,17 +19,17 @@ func TestError(t *testing.T) {
Type: ErrorTypePrivate, Type: ErrorTypePrivate,
} }
assert.Equal(t, err.Error(), baseError.Error()) 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.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.SetMeta("some data"), err)
assert.Equal(t, err.Meta, "some data") assert.Equal(t, "some data", err.Meta)
assert.Equal(t, err.JSON(), H{ assert.Equal(t, H{
"error": baseError.Error(), "error": baseError.Error(),
"meta": "some data", "meta": "some data",
}) }, err.JSON())
jsonBytes, _ := json.Marshal(err) jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes)) assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
@ -38,22 +38,22 @@ func TestError(t *testing.T) {
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) })
assert.Equal(t, err.JSON(), H{ assert.Equal(t, H{
"error": baseError.Error(), "error": baseError.Error(),
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) }, err.JSON())
err.SetMeta(H{ err.SetMeta(H{
"error": "custom error", "error": "custom error",
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) })
assert.Equal(t, err.JSON(), H{ assert.Equal(t, H{
"error": "custom error", "error": "custom error",
"status": "200", "status": "200",
"data": "some data", "data": "some data",
}) }, err.JSON())
type customError struct { type customError struct {
status string status string

View File

@ -1,7 +1,8 @@
# Guide to run Gin under App Engine LOCAL Development Server # Guide to run Gin under App Engine LOCAL Development Server
1. Download, install and setup Go in your computer. (That includes setting your `$GOPATH`.) 1. Download, install and setup Go in your computer. (That includes setting your `$GOPATH`.)
2. Download SDK for your platform from here: `https://developers.google.com/appengine/downloads?hl=es#Google_App_Engine_SDK_for_Go` 2. Download SDK for your platform from [here](https://cloud.google.com/appengine/docs/standard/go/download): `https://cloud.google.com/appengine/docs/standard/go/download`
3. Download Gin source code using: `$ go get github.com/gin-gonic/gin` 3. Download Gin source code using: `$ go get github.com/gin-gonic/gin`
4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/` 4. Navigate to examples folder: `$ cd $GOPATH/src/github.com/gin-gonic/gin/examples/app-engine/`
5. Run it: `$ goapp serve app-engine/` 5. Run it: `$ dev_appserver.py .` (notice that you have to run this script by Python2)

View File

@ -0,0 +1,33 @@
# Building a single binary containing templates
This is a complete example to create a single binary with the
[gin-gonic/gin][gin] Web Server with HTML templates.
[gin]: https://github.com/gin-gonic/gin
## How to use
### Prepare Packages
```
go get github.com/gin-gonic/gin
go get github.com/jessevdk/go-assets-builder
```
### Generate assets.go
```
go-assets-builder html -o assets.go
```
### Build the server
```
go build -o assets-in-binary
```
### Run
```
./assets-in-binary
```

View File

@ -0,0 +1,34 @@
package main
import (
"time"
"github.com/jessevdk/go-assets"
)
var _Assetsbfa8d115ce0617d89507412d5393a462f8e9b003 = "<!doctype html>\n<body>\n <p>Can you see this? → {{.Bar}}</p>\n</body>\n"
var _Assets3737a75b5254ed1f6d588b40a3449721f9ea86c2 = "<!doctype html>\n<body>\n <p>Hello, {{.Foo}}</p>\n</body>\n"
// Assets returns go-assets FileSystem
var Assets = assets.NewFileSystem(map[string][]string{"/": {"html"}, "/html": {"bar.tmpl", "index.tmpl"}}, map[string]*assets.File{
"/": {
Path: "/",
FileMode: 0x800001ed,
Mtime: time.Unix(1524365738, 1524365738517125470),
Data: nil,
}, "/html": {
Path: "/html",
FileMode: 0x800001ed,
Mtime: time.Unix(1524365491, 1524365491289799093),
Data: nil,
}, "/html/bar.tmpl": {
Path: "/html/bar.tmpl",
FileMode: 0x1a4,
Mtime: time.Unix(1524365491, 1524365491289611557),
Data: []byte(_Assetsbfa8d115ce0617d89507412d5393a462f8e9b003),
}, "/html/index.tmpl": {
Path: "/html/index.tmpl",
FileMode: 0x1a4,
Mtime: time.Unix(1524365491, 1524365491289995821),
Data: []byte(_Assets3737a75b5254ed1f6d588b40a3449721f9ea86c2),
}}, "")

View File

@ -0,0 +1,4 @@
<!doctype html>
<body>
<p>Can you see this? → {{.Bar}}</p>
</body>

View File

@ -0,0 +1,4 @@
<!doctype html>
<body>
<p>Hello, {{.Foo}}</p>
</body>

View File

@ -0,0 +1,48 @@
package main
import (
"html/template"
"io/ioutil"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.New()
t, err := loadTemplate()
if err != nil {
panic(err)
}
r.SetHTMLTemplate(t)
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "/html/index.tmpl", gin.H{
"Foo": "World",
})
})
r.GET("/bar", func(c *gin.Context) {
c.HTML(http.StatusOK, "/html/bar.tmpl", gin.H{
"Bar": "World",
})
})
r.Run(":8080")
}
func loadTemplate() (*template.Template, error) {
t := template.New("")
for name, file := range Assets.Files {
if file.IsDir() || !strings.HasSuffix(name, ".tmpl") {
continue
}
h, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}
t, err = t.New(name).Parse(string(h))
if err != nil {
return nil, err
}
}
return t, nil
}

View File

@ -1,19 +1,21 @@
package main package main
import ( import (
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var DB = make(map[string]string) var DB = make(map[string]string)
func main() { func setupRouter() *gin.Engine {
// Disable Console Color // Disable Console Color
// gin.DisableConsoleColor() // gin.DisableConsoleColor()
r := gin.Default() r := gin.Default()
// Ping test // Ping test
r.GET("/ping", func(c *gin.Context) { r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong") c.String(http.StatusOK, "pong")
}) })
// Get user value // Get user value
@ -21,9 +23,9 @@ func main() {
user := c.Params.ByName("name") user := c.Params.ByName("name")
value, ok := DB[user] value, ok := DB[user]
if ok { if ok {
c.JSON(200, gin.H{"user": user, "value": value}) c.JSON(http.StatusOK, gin.H{"user": user, "value": value})
} else { } else {
c.JSON(200, gin.H{"user": user, "status": "no value"}) c.JSON(http.StatusOK, gin.H{"user": user, "status": "no value"})
} }
}) })
@ -49,10 +51,15 @@ func main() {
if c.Bind(&json) == nil { if c.Bind(&json) == nil {
DB[user] = json.Value DB[user] = json.Value
c.JSON(200, gin.H{"status": "ok"}) c.JSON(http.StatusOK, gin.H{"status": "ok"})
} }
}) })
return r
}
func main() {
r := setupRouter()
// Listen and Server in 0.0.0.0:8080 // Listen and Server in 0.0.0.0:8080
r.Run(":8080") r.Run(":8080")
} }

View File

@ -0,0 +1,20 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPingRoute(t *testing.T) {
router := setupRouter()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/ping", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "pong", w.Body.String())
}

View File

@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
validator "gopkg.in/go-playground/validator.v8" "gopkg.in/go-playground/validator.v8"
) )
type Booking struct { type Booking struct {
@ -30,7 +30,11 @@ func bookableDate(
func main() { func main() {
route := gin.Default() route := gin.Default()
binding.Validator.RegisterValidation("bookabledate", bookableDate)
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable) route.GET("/bookable", getBookable)
route.Run(":8085") route.Run(":8085")
} }

View File

@ -1,6 +1,8 @@
package main package main
import ( import (
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/thinkerou/favicon" "github.com/thinkerou/favicon"
) )
@ -9,7 +11,7 @@ func main() {
app := gin.Default() app := gin.Default()
app.Use(favicon.New("./favicon.ico")) app.Use(favicon.New("./favicon.ico"))
app.GET("/ping", func(c *gin.Context) { app.GET("/ping", func(c *gin.Context) {
c.String(200, "Hello favicon.") c.String(http.StatusOK, "Hello favicon.")
}) })
app.Run(":8080") app.Run(":8080")
} }

View File

@ -27,8 +27,8 @@ func main() {
go func() { go func() {
// service connections // service connections
if err := srv.ListenAndServe(); err != nil { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("listen: %s\n", err) log.Fatalf("listen: %s\n", err)
} }
}() }()

19
examples/grpc/README.md Normal file
View File

@ -0,0 +1,19 @@
## How to run this example
1. run grpc server
```sh
$ go run grpc/server.go
```
2. run gin server
```sh
$ go run gin/main.go
```
3. use curl command to test it
```sh
$ curl -v 'http://localhost:8052/rest/n/thinkerou'
```

46
examples/grpc/gin/main.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
pb "github.com/gin-gonic/gin/examples/grpc/pb"
"google.golang.org/grpc"
)
func main() {
// Set up a connection to the server.
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewGreeterClient(conn)
// Set up a http setver.
r := gin.Default()
r.GET("/rest/n/:name", func(c *gin.Context) {
name := c.Param("name")
// Contact the server and print out its response.
req := &pb.HelloRequest{Name: name}
res, err := client.SayHello(c, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"result": fmt.Sprint(res.Message),
})
})
// Run http server
if err := r.Run(":8052"); err != nil {
log.Fatalf("could not run server: %v", err)
}
}

View File

@ -0,0 +1,34 @@
package main
import (
"log"
"net"
pb "github.com/gin-gonic/gin/examples/grpc/pb"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
// server is used to implement helloworld.GreeterServer.
type server struct{}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
// Register reflection service on gRPC server.
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

View File

@ -0,0 +1,151 @@
// Code generated by protoc-gen-go.
// source: helloworld.proto
// DO NOT EDIT!
/*
Package helloworld is a generated protocol buffer package.
It is generated from these files:
helloworld.proto
It has these top-level messages:
HelloRequest
HelloReply
*/
package helloworld
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import (
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
// The request message containing the user's name.
type HelloRequest struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
}
func (m *HelloRequest) Reset() { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage() {}
func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
// The response message containing the greetings
type HelloReply struct {
Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
}
func (m *HelloReply) Reset() { *m = HelloReply{} }
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
func (*HelloReply) ProtoMessage() {}
func (*HelloReply) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
func init() {
proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest")
proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply")
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// Client API for Greeter service
type GreeterClient interface {
// Sends a greeting
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}
type greeterClient struct {
cc *grpc.ClientConn
}
func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
return &greeterClient{cc}
}
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
out := new(HelloReply)
err := grpc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, c.cc, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Greeter service
type GreeterServer interface {
// Sends a greeting
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
s.RegisterService(&_Greeter_serviceDesc, srv)
}
func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(GreeterServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/helloworld.Greeter/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
var _Greeter_serviceDesc = grpc.ServiceDesc{
ServiceName: "helloworld.Greeter",
HandlerType: (*GreeterServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SayHello",
Handler: _Greeter_SayHello_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "helloworld.proto",
}
func init() { proto.RegisterFile("helloworld.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 174 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9,
0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88,
0x28, 0x29, 0x71, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42,
0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x92,
0x1a, 0x17, 0x17, 0x54, 0x4d, 0x41, 0x4e, 0xa5, 0x90, 0x04, 0x17, 0x7b, 0x6e, 0x6a, 0x71, 0x71,
0x62, 0x3a, 0x4c, 0x11, 0x8c, 0x6b, 0xe4, 0xc9, 0xc5, 0xee, 0x5e, 0x94, 0x9a, 0x5a, 0x92, 0x5a,
0x24, 0x64, 0xc7, 0xc5, 0x11, 0x9c, 0x58, 0x09, 0xd6, 0x25, 0x24, 0xa1, 0x87, 0xe4, 0x02, 0x64,
0xcb, 0xa4, 0xc4, 0xb0, 0xc8, 0x00, 0xad, 0x50, 0x62, 0x70, 0x32, 0xe0, 0x92, 0xce, 0xcc, 0xd7,
0x4b, 0x2f, 0x2a, 0x48, 0xd6, 0x4b, 0xad, 0x48, 0xcc, 0x2d, 0xc8, 0x49, 0x2d, 0x46, 0x52, 0xeb,
0xc4, 0x0f, 0x56, 0x1c, 0x0e, 0x62, 0x07, 0x80, 0xbc, 0x14, 0xc0, 0x98, 0xc4, 0x06, 0xf6, 0x9b,
0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0x0f, 0xb7, 0xcd, 0xf2, 0xef, 0x00, 0x00, 0x00,
}

View File

@ -0,0 +1,37 @@
// Copyright 2015 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

View File

@ -0,0 +1 @@
console.log("http2 pusher");

View File

@ -0,0 +1,41 @@
package main
import (
"html/template"
"log"
"github.com/gin-gonic/gin"
)
var html = template.Must(template.New("https").Parse(`
<html>
<head>
<title>Https Test</title>
<script src="/assets/app.js"></script>
</head>
<body>
<h1 style="color:red;">Welcome, Ginner!</h1>
</body>
</html>
`))
func main() {
r := gin.Default()
r.Static("/assets", "./assets")
r.SetHTMLTemplate(html)
r.GET("/", func(c *gin.Context) {
if pusher := c.Writer.Pusher(); pusher != nil {
// use pusher.Push() to do server push
if err := pusher.Push("/assets/app.js", nil); err != nil {
log.Printf("Failed to push: %v", err)
}
}
c.HTML(200, "https", gin.H{
"status": "success",
})
})
// Listen and Server in https://127.0.0.1:8080
r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key")
}

15
examples/http-pusher/testdata/ca.pem vendored Normal file
View File

@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla
Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0
YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT
BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7
+L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu
g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd
Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV
HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau
sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m
oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG
Dfcog5wrJytaQ6UA0wE=
-----END CERTIFICATE-----

View File

@ -0,0 +1,16 @@
-----BEGIN PRIVATE KEY-----
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD
M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf
3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY
AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm
V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY
tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p
dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q
K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR
81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff
DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd
aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2
ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3
XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe
F98XJ7tIFfJq
-----END PRIVATE KEY-----

View File

@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE-----
MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET
MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx
MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV
BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50
ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco
LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg
zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd
9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw
CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy
em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G
CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6
hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh
y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8
-----END CERTIFICATE-----

View File

@ -3,6 +3,7 @@ package main
import ( import (
"html/template" "html/template"
"log" "log"
"net/http"
"os" "os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -27,7 +28,7 @@ func main() {
r.SetHTMLTemplate(html) r.SetHTMLTemplate(html)
r.GET("/welcome", func(c *gin.Context) { r.GET("/welcome", func(c *gin.Context) {
c.HTML(200, "https", gin.H{ c.HTML(http.StatusOK, "https", gin.H{
"status": "success", "status": "success",
}) })
}) })

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"html" "html"
"io" "io"
"net/http"
"strings" "strings"
"time" "time"
@ -21,12 +22,12 @@ func rateLimit(c *gin.Context) {
fmt.Println("ip blocked") fmt.Println("ip blocked")
} }
c.Abort() c.Abort()
c.String(503, "you were automatically banned :)") c.String(http.StatusServiceUnavailable, "you were automatically banned :)")
} }
} }
func index(c *gin.Context) { func index(c *gin.Context) {
c.Redirect(301, "/room/hn") c.Redirect(http.StatusMovedPermanently, "/room/hn")
} }
func roomGET(c *gin.Context) { func roomGET(c *gin.Context) {
@ -38,7 +39,7 @@ func roomGET(c *gin.Context) {
if len(nick) > 13 { if len(nick) > 13 {
nick = nick[0:12] + "..." 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, "roomid": roomid,
"nick": nick, "nick": nick,
"timestamp": time.Now().Unix(), "timestamp": time.Now().Unix(),
@ -55,7 +56,7 @@ func roomPOST(c *gin.Context) {
validMessage := len(message) > 1 && len(message) < 200 validMessage := len(message) > 1 && len(message) < 200
validNick := len(nick) > 1 && len(nick) < 14 validNick := len(nick) > 1 && len(nick) < 14
if !validMessage || !validNick { if !validMessage || !validNick {
c.JSON(400, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
"status": "failed", "status": "failed",
"error": "the message or nickname is too long", "error": "the message or nickname is too long",
}) })
@ -68,7 +69,7 @@ func roomPOST(c *gin.Context) {
} }
messages.Add("inbound", 1) messages.Add("inbound", 1)
room(roomid).Submit(post) room(roomid).Submit(post)
c.JSON(200, post) c.JSON(http.StatusOK, post)
} }
func streamRoom(c *gin.Context) { func streamRoom(c *gin.Context) {

View File

@ -29,8 +29,8 @@ func statsWorker() {
"timestamp": uint64(time.Now().Unix()), "timestamp": uint64(time.Now().Unix()),
"HeapInuse": stats.HeapInuse, "HeapInuse": stats.HeapInuse,
"StackInuse": stats.StackInuse, "StackInuse": stats.StackInuse,
"Mallocs": (stats.Mallocs - lastMallocs), "Mallocs": stats.Mallocs - lastMallocs,
"Frees": (stats.Frees - lastFrees), "Frees": stats.Frees - lastFrees,
"Inbound": uint64(messages.Get("inbound")), "Inbound": uint64(messages.Get("inbound")),
"Outbound": uint64(messages.Get("outbound")), "Outbound": uint64(messages.Get("outbound")),
"Connected": connectedUsers(), "Connected": connectedUsers(),

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@ -34,7 +35,7 @@ func stream(c *gin.Context) {
func roomGET(c *gin.Context) { func roomGET(c *gin.Context) {
roomid := c.Param("roomid") roomid := c.Param("roomid")
userid := fmt.Sprint(rand.Int31()) userid := fmt.Sprint(rand.Int31())
c.HTML(200, "chat_room", gin.H{ c.HTML(http.StatusOK, "chat_room", gin.H{
"roomid": roomid, "roomid": roomid,
"userid": userid, "userid": userid,
}) })
@ -46,7 +47,7 @@ func roomPOST(c *gin.Context) {
message := c.PostForm("message") message := c.PostForm("message")
room(roomid).Submit(userid + ": " + message) room(roomid).Submit(userid + ": " + message)
c.JSON(200, gin.H{ c.JSON(http.StatusOK, gin.H{
"status": "success", "status": "success",
"message": message, "message": message,
}) })

View File

@ -0,0 +1,50 @@
## Struct level validations
Validations can also be registered at the `struct` level when field level validations
don't make much sense. This can also be used to solve cross-field validation elegantly.
Additionally, it can be combined with tag validations. Struct Level validations run after
the structs tag validations.
### Example requests
```shell
# Validation errors are generated for struct tags as well as at the struct level
$ curl -s -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{}' | jq
{
"error": "Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required' tag\nKey: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag",
"message": "User validation failed!"
}
# Validation fails at the struct level because neither first name nor last name are present
$ curl -s -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{"email": "george@vandaley.com"}' | jq
{
"error": "Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'fnameorlname' tag\nKey: 'User.LastName' Error:Field validation for 'LastName' failed on the 'fnameorlname' tag",
"message": "User validation failed!"
}
# No validation errors when either first name or last name is present
$ curl -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{"fname": "George", "email": "george@vandaley.com"}'
{"message":"User validation successful."}
$ curl -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{"lname": "Contanza", "email": "george@vandaley.com"}'
{"message":"User validation successful."}
$ curl -X POST http://localhost:8085/user \
-H 'content-type: application/json' \
-d '{"fname": "George", "lname": "Costanza", "email": "george@vandaley.com"}'
{"message":"User validation successful."}
```
### Useful links
- Validator docs - https://godoc.org/gopkg.in/go-playground/validator.v8#Validate.RegisterStructValidation
- Struct level example - https://github.com/go-playground/validator/blob/v8.18.2/examples/struct-level/struct_level.go
- Validator release notes - https://github.com/go-playground/validator/releases/tag/v8.7

View File

@ -0,0 +1,64 @@
package main
import (
"net/http"
"reflect"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
validator "gopkg.in/go-playground/validator.v8"
)
// User contains user information.
type User struct {
FirstName string `json:"fname"`
LastName string `json:"lname"`
Email string `binding:"required,email"`
}
// UserStructLevelValidation contains custom struct level validations that don't always
// make sense at the field validation level. For example, this function validates that either
// FirstName or LastName exist; could have done that with a custom field validation but then
// would have had to add it to both fields duplicating the logic + overhead, this way it's
// only validated once.
//
// NOTE: you may ask why wouldn't not just do this outside of validator. Doing this way
// hooks right into validator and you can combine with validation tags and still have a
// common error output format.
func UserStructLevelValidation(v *validator.Validate, structLevel *validator.StructLevel) {
user := structLevel.CurrentStruct.Interface().(User)
if len(user.FirstName) == 0 && len(user.LastName) == 0 {
structLevel.ReportError(
reflect.ValueOf(user.FirstName), "FirstName", "fname", "fnameorlname",
)
structLevel.ReportError(
reflect.ValueOf(user.LastName), "LastName", "lname", "fnameorlname",
)
}
// plus can to more, even with different tag than "fnameorlname"
}
func main() {
route := gin.Default()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterStructValidation(UserStructLevelValidation, User{})
}
route.POST("/user", validateUser)
route.Run(":8085")
}
func validateUser(c *gin.Context) {
var u User
if err := c.ShouldBindJSON(&u); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "User validation successful."})
} else {
c.JSON(http.StatusBadRequest, gin.H{
"message": "User validation failed!",
"error": err.Error(),
})
}
}

View File

@ -20,7 +20,7 @@ func main() {
router.SetFuncMap(template.FuncMap{ router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate, "formatAsDate": formatAsDate,
}) })
router.LoadHTMLFiles("../../fixtures/basic/raw.tmpl") router.LoadHTMLFiles("../../testdata/template/raw.tmpl")
router.GET("/raw", func(c *gin.Context) { router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{

125
gin.go
View File

@ -16,7 +16,7 @@ import (
const ( const (
// Version is Framework's version. // Version is Framework's version.
Version = "v1.2" Version = "v1.3.0"
defaultMultipartMemory = 32 << 20 // 32 MB defaultMultipartMemory = 32 << 20 // 32 MB
) )
@ -49,16 +49,6 @@ type RoutesInfo []RouteInfo
// Create an instance of Engine, by using New() or Default() // Create an instance of Engine, by using New() or Default()
type Engine struct { type Engine struct {
RouterGroup RouterGroup
delims render.Delims
secureJsonPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
trees methodTrees
// Enables automatic redirection if the current route can't be matched but a // Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists. // handler for the path with (without) the trailing slash exists.
@ -102,6 +92,17 @@ type Engine struct {
// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm // Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
// method call. // method call.
MaxMultipartMemory int64 MaxMultipartMemory int64
delims render.Delims
secureJsonPrefix string
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
trees methodTrees
} }
var _ IRouter = &Engine{} var _ IRouter = &Engine{}
@ -159,25 +160,30 @@ func (engine *Engine) Delims(left, right string) *Engine {
return engine return engine
} }
// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON.
func (engine *Engine) SecureJsonPrefix(prefix string) *Engine { func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
engine.secureJsonPrefix = prefix engine.secureJsonPrefix = prefix
return engine return engine
} }
// LoadHTMLGlob loads HTML files identified by glob pattern
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLGlob(pattern string) {
left := engine.delims.Left left := engine.delims.Left
right := engine.delims.Right right := engine.delims.Right
templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
if IsDebugging() { 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} engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
return return
} }
templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
engine.SetHTMLTemplate(templ) engine.SetHTMLTemplate(templ)
} }
// LoadHTMLFiles loads a slice of HTML files
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLFiles(files ...string) { func (engine *Engine) LoadHTMLFiles(files ...string) {
if IsDebugging() { if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims} engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims}
@ -188,6 +194,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) {
engine.SetHTMLTemplate(templ) engine.SetHTMLTemplate(templ)
} }
// SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) { func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 { if len(engine.trees) > 0 {
debugPrintWARNINGSetHTMLTemplate() debugPrintWARNINGSetHTMLTemplate()
@ -196,6 +203,7 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)} engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)}
} }
// SetFuncMap sets the FuncMap used for template.FuncMap.
func (engine *Engine) SetFuncMap(funcMap template.FuncMap) { func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
engine.FuncMap = funcMap engine.FuncMap = funcMap
} }
@ -321,7 +329,7 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
} }
// HandleContext re-enter a context that has been rewritten. // HandleContext re-enter a context that has been rewritten.
// This can be done by setting c.Request.Path to your new target. // This can be done by setting c.Request.URL.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely. // Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) { func (engine *Engine) HandleContext(c *Context) {
c.reset() c.reset()
@ -341,43 +349,45 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
// Find root of the tree for the given HTTP method // Find root of the tree for the given HTTP method
t := engine.trees t := engine.trees
for i, tl := 0, len(t); i < tl; i++ { for i, tl := 0, len(t); i < tl; i++ {
if t[i].method == httpMethod { if t[i].method != httpMethod {
root := t[i].root continue
// Find route in tree }
handlers, params, tsr := root.getValue(path, c.Params, unescape) root := t[i].root
if handlers != nil { // Find route in tree
c.handlers = handlers handlers, params, tsr := root.getValue(path, c.Params, unescape)
c.Params = params if handlers != nil {
c.Next() c.handlers = handlers
c.writermem.WriteHeaderNow() c.Params = params
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return return
} }
if httpMethod != "CONNECT" && path != "/" { if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
if tsr && engine.RedirectTrailingSlash { return
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
} }
break
} }
break
} }
if engine.HandleMethodNotAllowed { if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees { for _, tree := range engine.trees {
if tree.method != httpMethod { if tree.method == httpMethod {
if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil { continue
c.handlers = engine.allNoMethod }
serveError(c, 405, default405Body) if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
return c.handlers = engine.allNoMethod
} serveError(c, http.StatusMethodNotAllowed, default405Body)
return
} }
} }
} }
c.handlers = engine.allNoRoute c.handlers = engine.allNoRoute
serveError(c, 404, default404Body) serveError(c, http.StatusNotFound, default404Body)
} }
var mimePlain = []string{MIMEPlain} var mimePlain = []string{MIMEPlain}
@ -385,28 +395,29 @@ var mimePlain = []string{MIMEPlain}
func serveError(c *Context, code int, defaultMessage []byte) { func serveError(c *Context, code int, defaultMessage []byte) {
c.writermem.status = code c.writermem.status = code
c.Next() c.Next()
if !c.writermem.Written() { if c.writermem.Written() {
if c.writermem.Status() == code { return
c.writermem.Header()["Content-Type"] = mimePlain
c.Writer.Write(defaultMessage)
} else {
c.writermem.WriteHeaderNow()
}
} }
if c.writermem.Status() == code {
c.writermem.Header()["Content-Type"] = mimePlain
c.Writer.Write(defaultMessage)
return
}
c.writermem.WriteHeaderNow()
return
} }
func redirectTrailingSlash(c *Context) { func redirectTrailingSlash(c *Context) {
req := c.Request req := c.Request
path := req.URL.Path 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" { if req.Method != "GET" {
code = 307 code = http.StatusTemporaryRedirect
} }
if len(path) > 1 && path[len(path)-1] == '/' { req.URL.Path = path + "/"
req.URL.Path = path[:len(path)-1] if length := len(path); length > 1 && path[length-1] == '/' {
} else { req.URL.Path = path[:length-1]
req.URL.Path = path + "/"
} }
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code) http.Redirect(c.Writer, req, req.URL.String(), code)
@ -417,14 +428,10 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request req := c.Request
path := req.URL.Path path := req.URL.Path
fixedPath, found := root.findCaseInsensitivePath( if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(path), trailingSlash); ok {
cleanPath(path), code := http.StatusMovedPermanently // Permanent redirect, request with GET method
trailingSlash,
)
if found {
code := 301 // Permanent redirect, request with GET method
if req.Method != "GET" { if req.Method != "GET" {
code = 307 code = http.StatusTemporaryRedirect
} }
req.URL.Path = string(fixedPath) req.URL.Path = string(fixedPath)
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())

View File

@ -128,7 +128,7 @@ func Run(addr ...string) (err error) {
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests. // RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // Note: this method will block the calling goroutine undefinitelly unless an error happens.
func RunTLS(addr string, certFile string, keyFile string) (err error) { func RunTLS(addr, certFile, keyFile string) (err error) {
return engine().RunTLS(addr, certFile, keyFile) return engine().RunTLS(addr, certFile, keyFile)
} }

View File

@ -94,7 +94,7 @@ func TestUnixSocket(t *testing.T) {
c, err := net.Dial("unix", "/tmp/unix_unit_test") c, err := net.Dial("unix", "/tmp/unix_unit_test")
assert.NoError(t, err) assert.NoError(t, err)
fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") fmt.Fprint(c, "GET /example HTTP/1.0\r\n\r\n")
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
var response string var response string
for scanner.Scan() { for scanner.Scan() {

View File

@ -5,6 +5,7 @@
package gin package gin
import ( import (
"crypto/tls"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil" "io/ioutil"
@ -21,15 +22,15 @@ func formatAsDate(t time.Time) string {
return fmt.Sprintf("%d/%02d/%02d", year, month, day) return fmt.Sprintf("%d/%02d/%02d", year, month, day)
} }
func setupHTMLFiles(t *testing.T) func() { func setupHTMLFiles(t *testing.T, mode string, tls bool) func() {
go func() { go func() {
SetMode(TestMode) SetMode(mode)
router := New() router := New()
router.Delims("{[{", "}]}") router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{ router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate, "formatAsDate": formatAsDate,
}) })
router.LoadHTMLFiles("./fixtures/basic/hello.tmpl", "./fixtures/basic/raw.tmpl") router.LoadHTMLFiles("./testdata/template/hello.tmpl", "./testdata/template/raw.tmpl")
router.GET("/test", func(c *Context) { router.GET("/test", func(c *Context) {
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
}) })
@ -38,22 +39,27 @@ func setupHTMLFiles(t *testing.T) func() {
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
}) })
}) })
router.Run(":8888") if tls {
// these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
router.RunTLS(":9999", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")
} else {
router.Run(":8888")
}
}() }()
t.Log("waiting 1 second for server startup") t.Log("waiting 1 second for server startup")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
return func() {} return func() {}
} }
func setupHTMLGlob(t *testing.T) func() { func setupHTMLGlob(t *testing.T, mode string, tls bool) func() {
go func() { go func() {
SetMode(DebugMode) SetMode(mode)
router := New() router := New()
router.Delims("{[{", "}]}") router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{ router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate, "formatAsDate": formatAsDate,
}) })
router.LoadHTMLGlob("./fixtures/basic/*") router.LoadHTMLGlob("./testdata/template/*")
router.GET("/test", func(c *Context) { router.GET("/test", func(c *Context) {
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"}) c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
}) })
@ -62,16 +68,20 @@ func setupHTMLGlob(t *testing.T) func() {
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
}) })
}) })
router.Run(":8888") if tls {
// these files generated by `go run $GOROOT/src/crypto/tls/generate_cert.go --host 127.0.0.1`
router.RunTLS(":9999", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")
} else {
router.Run(":8888")
}
}() }()
t.Log("waiting 1 second for server startup") t.Log("waiting 1 second for server startup")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
return func() {} return func() {}
} }
//TODO
func TestLoadHTMLGlob(t *testing.T) { func TestLoadHTMLGlob(t *testing.T) {
td := setupHTMLGlob(t) td := setupHTMLGlob(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/test") res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -83,9 +93,55 @@ func TestLoadHTMLGlob(t *testing.T) {
td() td()
} }
func TestLoadHTMLGlob2(t *testing.T) {
td := setupHTMLGlob(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLGlob3(t *testing.T) {
td := setupHTMLGlob(t, ReleaseMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLGlobUsingTLS(t *testing.T) {
td := setupHTMLGlob(t, DebugMode, true)
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get("https://127.0.0.1:9999/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLGlobFromFuncMap(t *testing.T) { func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
time.Now() time.Now()
td := setupHTMLGlob(t) td := setupHTMLGlob(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/raw") res, err := http.Get("http://127.0.0.1:8888/raw")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -97,9 +153,6 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
td() td()
} }
// func (engine *Engine) LoadHTMLFiles(files ...string) {
// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
func init() { func init() {
SetMode(TestMode) SetMode(TestMode)
} }
@ -117,17 +170,17 @@ func TestCreateEngine(t *testing.T) {
// router.LoadHTMLGlob("*.testtmpl") // router.LoadHTMLGlob("*.testtmpl")
// r := router.HTMLRender.(render.HTMLDebug) // r := router.HTMLRender.(render.HTMLDebug)
// assert.Empty(t, r.Files) // assert.Empty(t, r.Files)
// assert.Equal(t, r.Glob, "*.testtmpl") // assert.Equal(t, "*.testtmpl", r.Glob)
// //
// router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl") // router.LoadHTMLFiles("index.html.testtmpl", "login.html.testtmpl")
// r = router.HTMLRender.(render.HTMLDebug) // r = router.HTMLRender.(render.HTMLDebug)
// assert.Empty(t, r.Glob) // assert.Empty(t, r.Glob)
// assert.Equal(t, r.Files, []string{"index.html", "login.html"}) // assert.Equal(t, []string{"index.html", "login.html"}, r.Files)
// SetMode(TestMode) // SetMode(TestMode)
// } // }
func TestLoadHTMLFiles(t *testing.T) { func TestLoadHTMLFiles(t *testing.T) {
td := setupHTMLFiles(t) td := setupHTMLFiles(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/test") res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -138,9 +191,52 @@ func TestLoadHTMLFiles(t *testing.T) {
td() td()
} }
func TestLoadHTMLFiles2(t *testing.T) {
td := setupHTMLFiles(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLFiles3(t *testing.T) {
td := setupHTMLFiles(t, ReleaseMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLFilesUsingTLS(t *testing.T) {
td := setupHTMLFiles(t, TestMode, true)
// Use InsecureSkipVerify for avoiding `x509: certificate signed by unknown authority` error
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
res, err := client.Get("https://127.0.0.1:9999/test")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
td()
}
func TestLoadHTMLFilesFuncMap(t *testing.T) { func TestLoadHTMLFilesFuncMap(t *testing.T) {
time.Now() time.Now()
td := setupHTMLFiles(t) td := setupHTMLFiles(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/raw") res, err := http.Get("http://127.0.0.1:8888/raw")
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@ -152,10 +248,6 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
td() td()
} }
func TestLoadHTMLReleaseMode(t *testing.T) {
}
func TestAddRoute(t *testing.T) { func TestAddRoute(t *testing.T) {
router := New() router := New()
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})

View File

@ -293,7 +293,7 @@ func githubConfigRouter(router *Engine) {
for _, param := range c.Params { for _, param := range c.Params {
output[param.Key] = param.Value output[param.Key] = param.Value
} }
c.JSON(200, output) c.JSON(http.StatusOK, output)
}) })
} }
} }

View File

@ -6,9 +6,7 @@
package json package json
import ( import "encoding/json"
"encoding/json"
)
var ( var (
Marshal = json.Marshal Marshal = json.Marshal

View File

@ -6,9 +6,7 @@
package json package json
import ( import "github.com/json-iterator/go"
"github.com/json-iterator/go"
)
var ( var (
json = jsoniter.ConfigCompatibleWithStandardLibrary json = jsoniter.ConfigCompatibleWithStandardLibrary

View File

@ -7,6 +7,7 @@ package gin
import ( import (
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"time" "time"
@ -118,11 +119,11 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
func colorForStatus(code int) string { func colorForStatus(code int) string {
switch { switch {
case code >= 200 && code < 300: case code >= http.StatusOK && code < http.StatusMultipleChoices:
return green return green
case code >= 300 && code < 400: case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
return white return white
case code >= 400 && code < 500: case code >= http.StatusBadRequest && code < http.StatusInternalServerError:
return yellow return yellow
default: default:
return red return red

View File

@ -7,6 +7,7 @@ package gin
import ( import (
"bytes" "bytes"
"errors" "errors"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -82,21 +83,21 @@ func TestLogger(t *testing.T) {
} }
func TestColorForMethod(t *testing.T) { func TestColorForMethod(t *testing.T) {
assert.Equal(t, colorForMethod("GET"), string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), "get should be blue") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
assert.Equal(t, colorForMethod("POST"), string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), "post should be cyan") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
assert.Equal(t, colorForMethod("PUT"), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "put should be yellow") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
assert.Equal(t, colorForMethod("DELETE"), string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), "delete should be red") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red")
assert.Equal(t, colorForMethod("PATCH"), string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), "patch should be green") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green")
assert.Equal(t, colorForMethod("HEAD"), string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), "head should be magenta") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta")
assert.Equal(t, colorForMethod("OPTIONS"), string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), "options should be white") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForMethod("OPTIONS"), "options should be white")
assert.Equal(t, colorForMethod("TRACE"), string([]byte{27, 91, 48, 109}), "trace is not defined and should be the reset color") assert.Equal(t, string([]byte{27, 91, 48, 109}), colorForMethod("TRACE"), "trace is not defined and should be the reset color")
} }
func TestColorForStatus(t *testing.T) { func TestColorForStatus(t *testing.T) {
assert.Equal(t, colorForStatus(200), string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), "2xx should be green") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
assert.Equal(t, colorForStatus(301), string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), "3xx should be white") assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
assert.Equal(t, colorForStatus(404), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "4xx should be yellow") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
assert.Equal(t, colorForStatus(2), string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), "other things should be red") assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
} }
func TestErrorLogger(t *testing.T) { func TestErrorLogger(t *testing.T) {
@ -106,23 +107,23 @@ func TestErrorLogger(t *testing.T) {
c.Error(errors.New("this is an error")) c.Error(errors.New("this is an error"))
}) })
router.GET("/abort", func(c *Context) { router.GET("/abort", func(c *Context) {
c.AbortWithError(401, errors.New("no authorized")) c.AbortWithError(http.StatusUnauthorized, errors.New("no authorized"))
}) })
router.GET("/print", func(c *Context) { router.GET("/print", func(c *Context) {
c.Error(errors.New("this is an error")) c.Error(errors.New("this is an error"))
c.String(500, "hola!") c.String(http.StatusInternalServerError, "hola!")
}) })
w := performRequest(router, "GET", "/error") 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()) assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort") 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()) assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print") 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()) assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
} }

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"errors" "errors"
"net/http"
"strings" "strings"
"testing" "testing"
@ -37,7 +38,7 @@ func TestMiddlewareGeneralCase(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 200, w.Code) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ACDB", signature) assert.Equal(t, "ACDB", signature)
} }
@ -73,7 +74,7 @@ func TestMiddlewareNoRoute(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 404, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "ACEGHFDB", signature) assert.Equal(t, "ACEGHFDB", signature)
} }
@ -110,7 +111,7 @@ func TestMiddlewareNoMethodEnabled(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 405, w.Code) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
assert.Equal(t, "ACEGHFDB", signature) assert.Equal(t, "ACEGHFDB", signature)
} }
@ -147,7 +148,7 @@ func TestMiddlewareNoMethodDisabled(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 404, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "AC X DB", signature) assert.Equal(t, "AC X DB", signature)
} }
@ -159,7 +160,7 @@ func TestMiddlewareAbort(t *testing.T) {
}) })
router.Use(func(c *Context) { router.Use(func(c *Context) {
signature += "C" signature += "C"
c.AbortWithStatus(401) c.AbortWithStatus(http.StatusUnauthorized)
c.Next() c.Next()
signature += "D" signature += "D"
}) })
@ -173,7 +174,7 @@ func TestMiddlewareAbort(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 401, w.Code) assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "ACD", signature) assert.Equal(t, "ACD", signature)
} }
@ -183,7 +184,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
router.Use(func(c *Context) { router.Use(func(c *Context) {
signature += "A" signature += "A"
c.Next() c.Next()
c.AbortWithStatus(410) c.AbortWithStatus(http.StatusGone)
signature += "B" signature += "B"
}) })
@ -195,7 +196,7 @@ func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 410, w.Code) assert.Equal(t, http.StatusGone, w.Code)
assert.Equal(t, "ACB", signature) assert.Equal(t, "ACB", signature)
} }
@ -207,7 +208,7 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
router := New() router := New()
router.Use(func(context *Context) { router.Use(func(context *Context) {
signature += "A" signature += "A"
context.AbortWithError(500, errors.New("foo")) context.AbortWithError(http.StatusInternalServerError, errors.New("foo"))
}) })
router.Use(func(context *Context) { router.Use(func(context *Context) {
signature += "B" signature += "B"
@ -218,25 +219,25 @@ func TestMiddlewareFailHandlersChain(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
// TEST // TEST
assert.Equal(t, 500, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "A", signature) assert.Equal(t, "A", signature)
} }
func TestMiddlewareWrite(t *testing.T) { func TestMiddlewareWrite(t *testing.T) {
router := New() router := New()
router.Use(func(c *Context) { router.Use(func(c *Context) {
c.String(400, "hola\n") c.String(http.StatusBadRequest, "hola\n")
}) })
router.Use(func(c *Context) { router.Use(func(c *Context) {
c.XML(400, H{"foo": "bar"}) c.XML(http.StatusBadRequest, H{"foo": "bar"})
}) })
router.Use(func(c *Context) { router.Use(func(c *Context) {
c.JSON(400, H{"foo": "bar"}) c.JSON(http.StatusBadRequest, H{"foo": "bar"})
}) })
router.GET("/", func(c *Context) { router.GET("/", func(c *Context) {
c.JSON(400, H{"foo": "bar"}) c.JSON(http.StatusBadRequest, H{"foo": "bar"})
}, func(c *Context) { }, func(c *Context) {
c.Render(400, sse.Event{ c.Render(http.StatusBadRequest, sse.Event{
Event: "test", Event: "test",
Data: "message", Data: "message",
}) })
@ -244,6 +245,6 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/") 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)) 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

@ -14,9 +14,9 @@ import (
const ENV_GIN_MODE = "GIN_MODE" const ENV_GIN_MODE = "GIN_MODE"
const ( const (
DebugMode string = "debug" DebugMode = "debug"
ReleaseMode string = "release" ReleaseMode = "release"
TestMode string = "test" TestMode = "test"
) )
const ( const (
debugCode = iota debugCode = iota
@ -39,16 +39,12 @@ var modeName = DebugMode
func init() { func init() {
mode := os.Getenv(ENV_GIN_MODE) mode := os.Getenv(ENV_GIN_MODE)
if mode == "" { SetMode(mode)
SetMode(DebugMode)
} else {
SetMode(mode)
}
} }
func SetMode(value string) { func SetMode(value string) {
switch value { switch value {
case DebugMode: case DebugMode, "":
ginMode = debugCode ginMode = debugCode
case ReleaseMode: case ReleaseMode:
ginMode = releaseCode ginMode = releaseCode
@ -57,6 +53,9 @@ func SetMode(value string) {
default: default:
panic("gin mode unknown: " + value) panic("gin mode unknown: " + value)
} }
if value == "" {
value = DebugMode
}
modeName = value modeName = value
} }

View File

@ -17,21 +17,25 @@ func init() {
} }
func TestSetMode(t *testing.T) { func TestSetMode(t *testing.T) {
assert.Equal(t, ginMode, testCode) assert.Equal(t, testCode, ginMode)
assert.Equal(t, Mode(), TestMode) assert.Equal(t, TestMode, Mode())
os.Unsetenv(ENV_GIN_MODE) os.Unsetenv(ENV_GIN_MODE)
SetMode("")
assert.Equal(t, debugCode, ginMode)
assert.Equal(t, DebugMode, Mode())
SetMode(DebugMode) SetMode(DebugMode)
assert.Equal(t, ginMode, debugCode) assert.Equal(t, debugCode, ginMode)
assert.Equal(t, Mode(), DebugMode) assert.Equal(t, DebugMode, Mode())
SetMode(ReleaseMode) SetMode(ReleaseMode)
assert.Equal(t, ginMode, releaseCode) assert.Equal(t, releaseCode, ginMode)
assert.Equal(t, Mode(), ReleaseMode) assert.Equal(t, ReleaseMode, Mode())
SetMode(TestMode) SetMode(TestMode)
assert.Equal(t, ginMode, testCode) assert.Equal(t, testCode, ginMode)
assert.Equal(t, Mode(), TestMode) assert.Equal(t, TestMode, Mode())
assert.Panics(t, func() { SetMode("unknown") }) assert.Panics(t, func() { SetMode("unknown") })
} }

View File

@ -41,7 +41,7 @@ func cleanPath(p string) string {
buf[0] = '/' buf[0] = '/'
} }
trailing := n > 2 && p[n-1] == '/' trailing := n > 1 && p[n-1] == '/'
// A bit more clunky without a 'lazybuf' like the path package, but the loop // A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp). So in contrast to the path package this // gets completely inlined (bufApp). So in contrast to the path package this
@ -59,11 +59,11 @@ func cleanPath(p string) string {
case p[r] == '.' && p[r+1] == '/': case p[r] == '.' && p[r+1] == '/':
// . element // . element
r++ r += 2
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last / // .. element: remove to last /
r += 2 r += 3
if w > 1 { if w > 1 {
// can backtrack // can backtrack

View File

@ -24,6 +24,7 @@ var cleanTests = []struct {
// missing root // missing root
{"", "/"}, {"", "/"},
{"a/", "/a/"},
{"abc", "/abc"}, {"abc", "/abc"},
{"abc/def", "/abc/def"}, {"abc/def", "/abc/def"},
{"a/b/c", "/a/b/c"}, {"a/b/c", "/a/b/c"},
@ -67,8 +68,8 @@ var cleanTests = []struct {
func TestPathClean(t *testing.T) { func TestPathClean(t *testing.T) {
for _, test := range cleanTests { for _, test := range cleanTests {
assert.Equal(t, cleanPath(test.path), test.result) assert.Equal(t, test.result, cleanPath(test.path))
assert.Equal(t, cleanPath(test.result), test.result) assert.Equal(t, test.result, cleanPath(test.result))
} }
} }

View File

@ -10,8 +10,10 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http"
"net/http/httputil" "net/http/httputil"
"runtime" "runtime"
"time"
) )
var ( var (
@ -38,9 +40,9 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
if logger != nil { if logger != nil {
stack := stack(3) stack := stack(3)
httprequest, _ := httputil.DumpRequest(c.Request, false) httprequest, _ := httputil.DumpRequest(c.Request, false)
logger.Printf("[Recovery] panic recovered:\n%s\n%s\n%s%s", string(httprequest), err, stack, reset) logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset)
} }
c.AbortWithStatus(500) c.AbortWithStatus(http.StatusInternalServerError)
} }
}() }()
c.Next() c.Next()
@ -107,3 +109,8 @@ func function(pc uintptr) []byte {
name = bytes.Replace(name, centerDot, dot, -1) name = bytes.Replace(name, centerDot, dot, -1)
return name return name
} }
func timeFormat(t time.Time) string {
var timeString = t.Format("2006/01/02 - 15:04:05")
return timeString
}

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"bytes" "bytes"
"net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -22,7 +23,7 @@ func TestPanicInHandler(t *testing.T) {
// RUN // RUN
w := performRequest(router, "GET", "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, w.Code, 500) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "GET /recovery") assert.Contains(t, buffer.String(), "GET /recovery")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "TestPanicInHandler") assert.Contains(t, buffer.String(), "TestPanicInHandler")
@ -33,11 +34,31 @@ func TestPanicWithAbort(t *testing.T) {
router := New() router := New()
router.Use(RecoveryWithWriter(nil)) router.Use(RecoveryWithWriter(nil))
router.GET("/recovery", func(c *Context) { router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(400) c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })
// RUN // RUN
w := performRequest(router, "GET", "/recovery") w := performRequest(router, "GET", "/recovery")
// TEST // TEST
assert.Equal(t, w.Code, 400) 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)
} }

View File

@ -6,6 +6,8 @@ package render
import ( import (
"bytes" "bytes"
"fmt"
"html/template"
"net/http" "net/http"
"github.com/gin-gonic/gin/json" "github.com/gin-gonic/gin/json"
@ -24,9 +26,20 @@ type SecureJSON struct {
Data interface{} Data interface{}
} }
type JsonpJSON struct {
Callback string
Data interface{}
}
type AsciiJSON struct {
Data interface{}
}
type SecureJSONPrefix string type SecureJSONPrefix string
var jsonContentType = []string{"application/json; charset=utf-8"} var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"}
func (r JSON) Render(w http.ResponseWriter) (err error) { func (r JSON) Render(w http.ResponseWriter) (err error) {
if err = WriteJSON(w, r.Data); err != nil { if err = WriteJSON(w, r.Data); err != nil {
@ -80,3 +93,54 @@ func (r SecureJSON) Render(w http.ResponseWriter) error {
func (r SecureJSON) WriteContentType(w http.ResponseWriter) { func (r SecureJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
} }
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
if err != nil {
return err
}
if r.Callback == "" {
w.Write(ret)
return nil
}
callback := template.JSEscapeString(r.Callback)
w.Write([]byte(callback))
w.Write([]byte("("))
w.Write(ret)
w.Write([]byte(")"))
return nil
}
func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonpContentType)
}
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 := ""
if r < 128 {
cvt = string(r)
} else {
cvt = fmt.Sprintf("\\u%04x", int64(r))
}
buffer.WriteString(cvt)
}
w.Write(buffer.Bytes())
return nil
}
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType)
}

View File

@ -26,6 +26,6 @@ func (r MsgPack) Render(w http.ResponseWriter) error {
func WriteMsgPack(w http.ResponseWriter, obj interface{}) error { func WriteMsgPack(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, msgpackContentType) writeContentType(w, msgpackContentType)
var h codec.Handle = new(codec.MsgpackHandle) var mh codec.MsgpackHandle
return codec.NewEncoder(w, h).Encode(obj) return codec.NewEncoder(w, &mh).Encode(obj)
} }

33
render/protobuf.go Normal file
View File

@ -0,0 +1,33 @@
// 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"
)
type ProtoBuf struct {
Data interface{}
}
var protobufContentType = []string{"application/x-protobuf"}
func (r ProtoBuf) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := proto.Marshal(r.Data.(proto.Message))
if err != nil {
return err
}
w.Write(bytes)
return nil
}
func (r ProtoBuf) WriteContentType(w http.ResponseWriter) {
writeContentType(w, protobufContentType)
}

40
render/reader.go Normal file
View File

@ -0,0 +1,40 @@
// 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 (
"io"
"net/http"
"strconv"
)
type Reader struct {
ContentType string
ContentLength int64
Reader io.Reader
Headers map[string]string
}
// Render (Reader) writes data with custom ContentType and headers.
func (r Reader) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
r.writeHeaders(w, r.Headers)
_, err = io.Copy(w, r.Reader)
return
}
func (r Reader) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType})
}
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
header := w.Header()
for k, v := range headers {
if val := header[k]; len(val) == 0 {
header[k] = []string{v}
}
}
}

View File

@ -16,6 +16,8 @@ type Redirect struct {
} }
func (r Redirect) Render(w http.ResponseWriter) error { 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 { if (r.Code < 300 || r.Code > 308) && r.Code != 201 {
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code)) panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
} }

View File

@ -15,6 +15,7 @@ var (
_ Render = JSON{} _ Render = JSON{}
_ Render = IndentedJSON{} _ Render = IndentedJSON{}
_ Render = SecureJSON{} _ Render = SecureJSON{}
_ Render = JsonpJSON{}
_ Render = XML{} _ Render = XML{}
_ Render = String{} _ Render = String{}
_ Render = Redirect{} _ Render = Redirect{}
@ -24,6 +25,9 @@ var (
_ HTMLRender = HTMLProduction{} _ HTMLRender = HTMLProduction{}
_ Render = YAML{} _ Render = YAML{}
_ Render = MsgPack{} _ Render = MsgPack{}
_ Render = Reader{}
_ Render = AsciiJSON{}
_ Render = ProtoBuf{}
) )
func writeContentType(w http.ResponseWriter, value []string) { func writeContentType(w http.ResponseWriter, value []string) {

View File

@ -7,12 +7,19 @@ package render
import ( import (
"bytes" "bytes"
"encoding/xml" "encoding/xml"
"errors"
"html/template" "html/template"
"net/http"
"net/http/httptest" "net/http/httptest"
"strconv"
"strings"
"testing" "testing"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
) )
// TODO unit tests // TODO unit tests
@ -24,6 +31,9 @@ func TestRenderMsgPack(t *testing.T) {
"foo": "bar", "foo": "bar",
} }
(MsgPack{data}).WriteContentType(w)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
err := (MsgPack{data}).Render(w) err := (MsgPack{data}).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
@ -36,7 +46,7 @@ func TestRenderMsgPack(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, w.Body.String(), string(buf.Bytes())) assert.Equal(t, w.Body.String(), string(buf.Bytes()))
assert.Equal(t, w.Header().Get("Content-Type"), "application/msgpack; charset=utf-8") assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderJSON(t *testing.T) { func TestRenderJSON(t *testing.T) {
@ -46,6 +56,9 @@ func TestRenderJSON(t *testing.T) {
"html": "<b>", "html": "<b>",
} }
(JSON{data}).WriteContentType(w)
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
err := (JSON{data}).Render(w) err := (JSON{data}).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
@ -54,6 +67,14 @@ func TestRenderJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderJSONPanics(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
// json: unsupported type: chan int
assert.Panics(t, func() { (JSON{data}).Render(w) })
}
func TestRenderIndentedJSON(t *testing.T) { func TestRenderIndentedJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
data := map[string]interface{}{ data := map[string]interface{}{
@ -64,8 +85,17 @@ func TestRenderIndentedJSON(t *testing.T) {
err := (IndentedJSON{data}).Render(w) err := (IndentedJSON{data}).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}") assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderIndentedJSONPanics(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
// json: unsupported type: chan int
err := (IndentedJSON{data}).Render(w)
assert.Error(t, err)
} }
func TestRenderSecureJSON(t *testing.T) { func TestRenderSecureJSON(t *testing.T) {
@ -74,6 +104,9 @@ func TestRenderSecureJSON(t *testing.T) {
"foo": "bar", "foo": "bar",
} }
(SecureJSON{"while(1);", data}).WriteContentType(w1)
assert.Equal(t, "application/json; charset=utf-8", w1.Header().Get("Content-Type"))
err1 := (SecureJSON{"while(1);", data}).Render(w1) err1 := (SecureJSON{"while(1);", data}).Render(w1)
assert.NoError(t, err1) assert.NoError(t, err1)
@ -93,6 +126,96 @@ func TestRenderSecureJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w2.Header().Get("Content-Type"))
} }
func TestRenderSecureJSONFail(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
// json: unsupported type: chan int
err := (SecureJSON{"while(1);", data}).Render(w)
assert.Error(t, err)
}
func TestRenderJsonpJSON(t *testing.T) {
w1 := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
}
(JsonpJSON{"x", data}).WriteContentType(w1)
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
err1 := (JsonpJSON{"x", data}).Render(w1)
assert.NoError(t, err1)
assert.Equal(t, "x({\"foo\":\"bar\"})", w1.Body.String())
assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
datas := []map[string]interface{}{{
"foo": "bar",
}, {
"bar": "foo",
}}
err2 := (JsonpJSON{"x", datas}).Render(w2)
assert.NoError(t, err2)
assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}])", w2.Body.String())
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)
// json: unsupported type: chan int
err := (JsonpJSON{"x", data}).Render(w)
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{} type xmlmap map[string]interface{}
// Allows type H to be used with xml.Marshal // Allows type H to be used with xml.Marshal
@ -113,10 +236,67 @@ func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return err return err
} }
} }
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return err return e.EncodeToken(xml.EndElement{Name: start.Name})
}
func TestRenderYAML(t *testing.T) {
w := httptest.NewRecorder()
data := `
a : Easy!
b:
c: 2
d: [3, 4]
`
(YAML{data}).WriteContentType(w)
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
err := (YAML{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, "\"\\na : Easy!\\nb:\\n\\tc: 2\\n\\td: [3, 4]\\n\\t\"\n", w.Body.String())
assert.Equal(t, "application/x-yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
type fail struct{}
// Hook MarshalYAML
func (ft *fail) MarshalYAML() (interface{}, error) {
return nil, errors.New("fail")
}
func TestRenderYAMLFail(t *testing.T) {
w := httptest.NewRecorder()
err := (YAML{&fail{}}).Render(w)
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,
} }
return nil
(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) { func TestRenderXML(t *testing.T) {
@ -125,15 +305,41 @@ func TestRenderXML(t *testing.T) {
"foo": "bar", "foo": "bar",
} }
(XML{data}).WriteContentType(w)
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
err := (XML{data}).Render(w) err := (XML{data}).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "<map><foo>bar</foo></map>") assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8") assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderRedirect(t *testing.T) { func TestRenderRedirect(t *testing.T) {
// TODO req, err := http.NewRequest("GET", "/test-redirect", nil)
assert.NoError(t, err)
data1 := Redirect{
Code: http.StatusMovedPermanently,
Request: req,
Location: "/new/location",
}
w := httptest.NewRecorder()
err = data1.Render(w)
assert.NoError(t, err)
data2 := Redirect{
Code: http.StatusOK,
Request: req,
Location: "/new/location",
}
w = httptest.NewRecorder()
assert.Panics(t, func() { data2.Render(w) })
// only improve coverage
data2.WriteContentType(w)
} }
func TestRenderData(t *testing.T) { func TestRenderData(t *testing.T) {
@ -146,21 +352,40 @@ func TestRenderData(t *testing.T) {
}).Render(w) }).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "#!PNG some raw data") assert.Equal(t, "#!PNG some raw data", w.Body.String())
assert.Equal(t, w.Header().Get("Content-Type"), "image/png") assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
} }
func TestRenderString(t *testing.T) { func TestRenderString(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
(String{
Format: "hello %s %d",
Data: []interface{}{},
}).WriteContentType(w)
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
err := (String{ err := (String{
Format: "hola %s %d", Format: "hola %s %d",
Data: []interface{}{"manu", 2}, Data: []interface{}{"manu", 2},
}).Render(w) }).Render(w)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "hola manu 2") assert.Equal(t, "hola manu 2", w.Body.String())
assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8") assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderStringLenZero(t *testing.T) {
w := httptest.NewRecorder()
err := (String{
Format: "hola %s %d",
Data: []interface{}{},
}).Render(w)
assert.NoError(t, err)
assert.Equal(t, "hola %s %d", w.Body.String())
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
} }
func TestRenderHTMLTemplate(t *testing.T) { func TestRenderHTMLTemplate(t *testing.T) {
@ -175,6 +400,88 @@ func TestRenderHTMLTemplate(t *testing.T) {
err := instance.Render(w) err := instance.Render(w)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "Hello alexandernyquist") assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderHTMLTemplateEmptyName(t *testing.T) {
w := httptest.NewRecorder()
templ := template.Must(template.New("").Parse(`Hello {{.name}}`))
htmlRender := HTMLProduction{Template: templ}
instance := htmlRender.Instance("", map[string]interface{}{
"name": "alexandernyquist",
})
err := instance.Render(w)
assert.NoError(t, err)
assert.Equal(t, "Hello alexandernyquist", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderHTMLDebugFiles(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{Files: []string{"../testdata/template/hello.tmpl"},
Glob: "",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{
"name": "thinkerou",
})
err := instance.Render(w)
assert.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderHTMLDebugGlob(t *testing.T) {
w := httptest.NewRecorder()
htmlRender := HTMLDebug{Files: nil,
Glob: "../testdata/template/hello*",
Delims: Delims{Left: "{[{", Right: "}]}"},
FuncMap: nil,
}
instance := htmlRender.Instance("hello.tmpl", map[string]interface{}{
"name": "thinkerou",
})
err := instance.Render(w)
assert.NoError(t, err)
assert.Equal(t, "<h1>Hello thinkerou</h1>", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderHTMLDebugPanics(t *testing.T) {
htmlRender := HTMLDebug{Files: nil,
Glob: "",
Delims: Delims{"{{", "}}"},
FuncMap: nil,
}
assert.Panics(t, func() { htmlRender.Instance("", nil) })
}
func TestRenderReader(t *testing.T) {
w := httptest.NewRecorder()
body := "#!PNG some raw data"
headers := make(map[string]string)
headers["Content-Disposition"] = `attachment; filename="filename.png"`
err := (Reader{
ContentLength: int64(len(body)),
ContentType: "image/png",
Reader: strings.NewReader(body),
Headers: headers,
}).Render(w)
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"))
} }

View File

@ -30,7 +30,7 @@ func WriteString(w http.ResponseWriter, format string, data []interface{}) {
writeContentType(w, plainContentType) writeContentType(w, plainContentType)
if len(data) > 0 { if len(data) > 0 {
fmt.Fprintf(w, format, data...) fmt.Fprintf(w, format, data...)
} else { return
io.WriteString(w, format)
} }
io.WriteString(w, format)
} }

View File

@ -13,10 +13,10 @@ import (
const ( const (
noWritten = -1 noWritten = -1
defaultStatus = 200 defaultStatus = http.StatusOK
) )
type ResponseWriter interface { type responseWriterBase interface {
http.ResponseWriter http.ResponseWriter
http.Hijacker http.Hijacker
http.Flusher http.Flusher
@ -110,5 +110,6 @@ func (w *responseWriter) CloseNotify() <-chan bool {
// Flush implements the http.Flush interface. // Flush implements the http.Flush interface.
func (w *responseWriter) Flush() { func (w *responseWriter) Flush() {
w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush() w.ResponseWriter.(http.Flusher).Flush()
} }

12
response_writer_1.7.go Normal file
View File

@ -0,0 +1,12 @@
// +build !go1.8
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
// ResponseWriter ...
type ResponseWriter interface {
responseWriterBase
}

25
response_writer_1.8.go Normal file
View File

@ -0,0 +1,25 @@
// +build go1.8
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"net/http"
)
// ResponseWriter ...
type ResponseWriter interface {
responseWriterBase
// get the http.Pusher for server push
Pusher() http.Pusher
}
func (w *responseWriter) Pusher() (pusher http.Pusher) {
if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
return pusher
}
return nil
}

View File

@ -35,10 +35,10 @@ func TestResponseWriterReset(t *testing.T) {
writer.reset(testWritter) writer.reset(testWritter)
assert.Equal(t, -1, writer.size) 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, testWritter, writer.ResponseWriter)
assert.Equal(t, -1, w.Size()) assert.Equal(t, -1, w.Size())
assert.Equal(t, 200, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
assert.False(t, w.Written()) assert.False(t, w.Written())
} }
@ -48,13 +48,13 @@ func TestResponseWriterWriteHeader(t *testing.T) {
writer.reset(testWritter) writer.reset(testWritter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
w.WriteHeader(300) w.WriteHeader(http.StatusMultipleChoices)
assert.False(t, w.Written()) assert.False(t, w.Written())
assert.Equal(t, 300, w.Status()) assert.Equal(t, http.StatusMultipleChoices, w.Status())
assert.NotEqual(t, testWritter.Code, 300) assert.NotEqual(t, http.StatusMultipleChoices, testWritter.Code)
w.WriteHeader(-1) w.WriteHeader(-1)
assert.Equal(t, 300, w.Status()) assert.Equal(t, http.StatusMultipleChoices, w.Status())
} }
func TestResponseWriterWriteHeadersNow(t *testing.T) { func TestResponseWriterWriteHeadersNow(t *testing.T) {
@ -63,12 +63,12 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
writer.reset(testWritter) writer.reset(testWritter)
w := ResponseWriter(writer) w := ResponseWriter(writer)
w.WriteHeader(300) w.WriteHeader(http.StatusMultipleChoices)
w.WriteHeaderNow() w.WriteHeaderNow()
assert.True(t, w.Written()) assert.True(t, w.Written())
assert.Equal(t, 0, w.Size()) assert.Equal(t, 0, w.Size())
assert.Equal(t, 300, testWritter.Code) assert.Equal(t, http.StatusMultipleChoices, testWritter.Code)
writer.size = 10 writer.size = 10
w.WriteHeaderNow() w.WriteHeaderNow()
@ -84,8 +84,8 @@ func TestResponseWriterWrite(t *testing.T) {
n, err := w.Write([]byte("hola")) n, err := w.Write([]byte("hola"))
assert.Equal(t, 4, n) assert.Equal(t, 4, n)
assert.Equal(t, 4, w.Size()) assert.Equal(t, 4, w.Size())
assert.Equal(t, 200, w.Status()) assert.Equal(t, http.StatusOK, w.Status())
assert.Equal(t, 200, testWritter.Code) assert.Equal(t, http.StatusOK, testWritter.Code)
assert.Equal(t, "hola", testWritter.Body.String()) assert.Equal(t, "hola", testWritter.Body.String())
assert.NoError(t, err) assert.NoError(t, err)
@ -113,3 +113,19 @@ func TestResponseWriterHijack(t *testing.T) {
w.Flush() 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

@ -139,7 +139,7 @@ func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRou
return group.returnObj() return group.returnObj()
} }
// StaticFile registers a single route in order to server a single file of the local filesystem. // StaticFile registers a single route in order to serve a single file of the local filesystem.
// router.StaticFile("favicon.ico", "./resources/favicon.ico") // router.StaticFile("favicon.ico", "./resources/favicon.ico")
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes { func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
@ -184,7 +184,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
_, nolisting := fs.(*onlyfilesFS) _, nolisting := fs.(*onlyfilesFS)
return func(c *Context) { return func(c *Context) {
if nolisting { if nolisting {
c.Writer.WriteHeader(404) c.Writer.WriteHeader(http.StatusNotFound)
} }
fileServer.ServeHTTP(c.Writer, c.Request) fileServer.ServeHTTP(c.Writer, c.Request)
} }

View File

@ -5,6 +5,7 @@
package gin package gin
import ( import (
"net/http"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -20,15 +21,15 @@ func TestRouterGroupBasic(t *testing.T) {
group.Use(func(c *Context) {}) group.Use(func(c *Context) {})
assert.Len(t, group.Handlers, 2) assert.Len(t, group.Handlers, 2)
assert.Equal(t, group.BasePath(), "/hola") assert.Equal(t, "/hola", group.BasePath())
assert.Equal(t, group.engine, router) assert.Equal(t, router, group.engine)
group2 := group.Group("manu") group2 := group.Group("manu")
group2.Use(func(c *Context) {}, func(c *Context) {}) group2.Use(func(c *Context) {}, func(c *Context) {})
assert.Len(t, group2.Handlers, 4) assert.Len(t, group2.Handlers, 4)
assert.Equal(t, group2.BasePath(), "/hola/manu") assert.Equal(t, "/hola/manu", group2.BasePath())
assert.Equal(t, group2.engine, router) assert.Equal(t, router, group2.engine)
} }
func TestRouterGroupBasicHandle(t *testing.T) { func TestRouterGroupBasicHandle(t *testing.T) {
@ -44,13 +45,13 @@ func TestRouterGroupBasicHandle(t *testing.T) {
func performRequestInGroup(t *testing.T, method string) { func performRequestInGroup(t *testing.T, method string) {
router := New() router := New()
v1 := router.Group("v1", func(c *Context) {}) v1 := router.Group("v1", func(c *Context) {})
assert.Equal(t, v1.BasePath(), "/v1") assert.Equal(t, "/v1", v1.BasePath())
login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {}) login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {})
assert.Equal(t, login.BasePath(), "/v1/login/") assert.Equal(t, "/v1/login/", login.BasePath())
handler := func(c *Context) { 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 { switch method {
@ -80,12 +81,12 @@ func performRequestInGroup(t *testing.T, method string) {
} }
w := performRequest(router, method, "/v1/login/test") w := performRequest(router, method, "/v1/login/test")
assert.Equal(t, w.Code, 400) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, w.Body.String(), "the method was "+method+" and index 3") assert.Equal(t, "the method was "+method+" and index 3", w.Body.String())
w = performRequest(router, method, "/v1/test") w = performRequest(router, method, "/v1/test")
assert.Equal(t, w.Code, 400) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, w.Body.String(), "the method was "+method+" and index 1") assert.Equal(t, "the method was "+method+" and index 1", w.Body.String())
} }
func TestRouterGroupInvalidStatic(t *testing.T) { func TestRouterGroupInvalidStatic(t *testing.T) {

View File

@ -36,7 +36,7 @@ func testRouteOK(method string, t *testing.T) {
w := performRequest(r, method, "/test") w := performRequest(r, method, "/test")
assert.True(t, passed) assert.True(t, passed)
assert.Equal(t, w.Code, http.StatusOK) assert.Equal(t, http.StatusOK, w.Code)
performRequest(r, method, "/test2") performRequest(r, method, "/test2")
assert.True(t, passedAny) assert.True(t, passedAny)
@ -53,7 +53,7 @@ func testRouteNotOK(method string, t *testing.T) {
w := performRequest(router, method, "/test") w := performRequest(router, method, "/test")
assert.False(t, passed) assert.False(t, passed)
assert.Equal(t, w.Code, http.StatusNotFound) assert.Equal(t, http.StatusNotFound, w.Code)
} }
// TestSingleRouteOK tests that POST route is correctly invoked. // TestSingleRouteOK tests that POST route is correctly invoked.
@ -74,27 +74,27 @@ func testRouteNotOK2(method string, t *testing.T) {
w := performRequest(router, method, "/test") w := performRequest(router, method, "/test")
assert.False(t, passed) assert.False(t, passed)
assert.Equal(t, w.Code, http.StatusMethodNotAllowed) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
} }
func TestRouterMethod(t *testing.T) { func TestRouterMethod(t *testing.T) {
router := New() router := New()
router.PUT("/hey2", func(c *Context) { router.PUT("/hey2", func(c *Context) {
c.String(200, "sup2") c.String(http.StatusOK, "sup2")
}) })
router.PUT("/hey", func(c *Context) { router.PUT("/hey", func(c *Context) {
c.String(200, "called") c.String(http.StatusOK, "called")
}) })
router.PUT("/hey3", func(c *Context) { router.PUT("/hey3", func(c *Context) {
c.String(200, "sup3") c.String(http.StatusOK, "sup3")
}) })
w := performRequest(router, "PUT", "/hey") w := performRequest(router, "PUT", "/hey")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, w.Body.String(), "called") assert.Equal(t, "called", w.Body.String())
} }
func TestRouterGroupRouteOK(t *testing.T) { func TestRouterGroupRouteOK(t *testing.T) {
@ -143,43 +143,43 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
router.PUT("/path4/", func(c *Context) {}) router.PUT("/path4/", func(c *Context) {})
w := performRequest(router, "GET", "/path/") w := performRequest(router, "GET", "/path/")
assert.Equal(t, w.Header().Get("Location"), "/path") assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, w.Code, 301) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, "GET", "/path2")
assert.Equal(t, w.Header().Get("Location"), "/path2/") assert.Equal(t, "/path2/", w.Header().Get("Location"))
assert.Equal(t, w.Code, 301) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "POST", "/path3/") w = performRequest(router, "POST", "/path3/")
assert.Equal(t, w.Header().Get("Location"), "/path3") assert.Equal(t, "/path3", w.Header().Get("Location"))
assert.Equal(t, w.Code, 307) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "PUT", "/path4") w = performRequest(router, "PUT", "/path4")
assert.Equal(t, w.Header().Get("Location"), "/path4/") assert.Equal(t, "/path4/", w.Header().Get("Location"))
assert.Equal(t, w.Code, 307) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "GET", "/path") w = performRequest(router, "GET", "/path")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "GET", "/path2/") w = performRequest(router, "GET", "/path2/")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "POST", "/path3") w = performRequest(router, "POST", "/path3")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
w = performRequest(router, "PUT", "/path4/") w = performRequest(router, "PUT", "/path4/")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
router.RedirectTrailingSlash = false router.RedirectTrailingSlash = false
w = performRequest(router, "GET", "/path/") w = performRequest(router, "GET", "/path/")
assert.Equal(t, w.Code, 404) assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, "GET", "/path2")
assert.Equal(t, w.Code, 404) assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "POST", "/path3/") w = performRequest(router, "POST", "/path3/")
assert.Equal(t, w.Code, 404) assert.Equal(t, http.StatusNotFound, w.Code)
w = performRequest(router, "PUT", "/path4") w = performRequest(router, "PUT", "/path4")
assert.Equal(t, w.Code, 404) assert.Equal(t, http.StatusNotFound, w.Code)
} }
func TestRouteRedirectFixedPath(t *testing.T) { func TestRouteRedirectFixedPath(t *testing.T) {
@ -193,20 +193,20 @@ func TestRouteRedirectFixedPath(t *testing.T) {
router.POST("/Path4/", func(c *Context) {}) router.POST("/Path4/", func(c *Context) {})
w := performRequest(router, "GET", "/PATH") w := performRequest(router, "GET", "/PATH")
assert.Equal(t, w.Header().Get("Location"), "/path") assert.Equal(t, "/path", w.Header().Get("Location"))
assert.Equal(t, w.Code, 301) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "GET", "/path2") w = performRequest(router, "GET", "/path2")
assert.Equal(t, w.Header().Get("Location"), "/Path2") assert.Equal(t, "/Path2", w.Header().Get("Location"))
assert.Equal(t, w.Code, 301) assert.Equal(t, http.StatusMovedPermanently, w.Code)
w = performRequest(router, "POST", "/path3") w = performRequest(router, "POST", "/path3")
assert.Equal(t, w.Header().Get("Location"), "/PATH3") assert.Equal(t, "/PATH3", w.Header().Get("Location"))
assert.Equal(t, w.Code, 307) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
w = performRequest(router, "POST", "/path4") w = performRequest(router, "POST", "/path4")
assert.Equal(t, w.Header().Get("Location"), "/Path4/") assert.Equal(t, "/Path4/", w.Header().Get("Location"))
assert.Equal(t, w.Code, 307) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
} }
// TestContextParamsGet tests that a parameter can be parsed from the URL. // TestContextParamsGet tests that a parameter can be parsed from the URL.
@ -236,10 +236,10 @@ func TestRouteParamsByName(t *testing.T) {
w := performRequest(router, "GET", "/test/john/smith/is/super/great") w := performRequest(router, "GET", "/test/john/smith/is/super/great")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, name, "john") assert.Equal(t, "john", name)
assert.Equal(t, lastName, "smith") assert.Equal(t, "smith", lastName)
assert.Equal(t, wild, "/is/super/great") assert.Equal(t, "/is/super/great", wild)
} }
// TestHandleStaticFile - ensure the static file handles properly // TestHandleStaticFile - ensure the static file handles properly
@ -265,15 +265,15 @@ func TestRouteStaticFile(t *testing.T) {
w2 := performRequest(router, "GET", "/result") w2 := performRequest(router, "GET", "/result")
assert.Equal(t, w, w2) assert.Equal(t, w, w2)
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, w.Body.String(), "Gin Web Framework") assert.Equal(t, "Gin Web Framework", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") assert.Equal(t, "text/plain; charset=utf-8", w.HeaderMap.Get("Content-Type"))
w3 := performRequest(router, "HEAD", "/using_static/"+filename) w3 := performRequest(router, "HEAD", "/using_static/"+filename)
w4 := performRequest(router, "HEAD", "/result") w4 := performRequest(router, "HEAD", "/result")
assert.Equal(t, w3, w4) assert.Equal(t, w3, w4)
assert.Equal(t, w3.Code, 200) assert.Equal(t, http.StatusOK, w3.Code)
} }
// TestHandleStaticDir - ensure the root/sub dir handles properly // TestHandleStaticDir - ensure the root/sub dir handles properly
@ -283,9 +283,9 @@ func TestRouteStaticListingDir(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "gin.go") assert.Contains(t, w.Body.String(), "gin.go")
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// TestHandleHeadToDir - ensure the root/sub dir handles properly // TestHandleHeadToDir - ensure the root/sub dir handles properly
@ -295,7 +295,7 @@ func TestRouteStaticNoListing(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 404) assert.Equal(t, http.StatusNotFound, w.Code)
assert.NotContains(t, w.Body.String(), "gin.go") assert.NotContains(t, w.Body.String(), "gin.go")
} }
@ -310,12 +310,12 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
w := performRequest(router, "GET", "/gin.go") w := performRequest(router, "GET", "/gin.go")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "package gin") assert.Contains(t, w.Body.String(), "package gin")
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") 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.NotEqual(t, w.HeaderMap.Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST")
assert.Equal(t, w.HeaderMap.Get("Expires"), "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, w.HeaderMap.Get("x-GIN"), "Gin Framework") assert.Equal(t, "Gin Framework", w.HeaderMap.Get("x-GIN"))
} }
func TestRouteNotAllowedEnabled(t *testing.T) { func TestRouteNotAllowedEnabled(t *testing.T) {
@ -323,14 +323,24 @@ func TestRouteNotAllowedEnabled(t *testing.T) {
router.HandleMethodNotAllowed = true router.HandleMethodNotAllowed = true
router.POST("/path", func(c *Context) {}) router.POST("/path", func(c *Context) {})
w := performRequest(router, "GET", "/path") w := performRequest(router, "GET", "/path")
assert.Equal(t, w.Code, http.StatusMethodNotAllowed) assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
router.NoMethod(func(c *Context) { router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText") c.String(http.StatusTeapot, "responseText")
}) })
w = performRequest(router, "GET", "/path") w = performRequest(router, "GET", "/path")
assert.Equal(t, w.Body.String(), "responseText") assert.Equal(t, "responseText", w.Body.String())
assert.Equal(t, w.Code, http.StatusTeapot) 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) { func TestRouteNotAllowedDisabled(t *testing.T) {
@ -338,14 +348,14 @@ func TestRouteNotAllowedDisabled(t *testing.T) {
router.HandleMethodNotAllowed = false router.HandleMethodNotAllowed = false
router.POST("/path", func(c *Context) {}) router.POST("/path", func(c *Context) {})
w := performRequest(router, "GET", "/path") w := performRequest(router, "GET", "/path")
assert.Equal(t, w.Code, 404) assert.Equal(t, http.StatusNotFound, w.Code)
router.NoMethod(func(c *Context) { router.NoMethod(func(c *Context) {
c.String(http.StatusTeapot, "responseText") c.String(http.StatusTeapot, "responseText")
}) })
w = performRequest(router, "GET", "/path") w = performRequest(router, "GET", "/path")
assert.Equal(t, w.Body.String(), "404 page not found") assert.Equal(t, "404 page not found", w.Body.String())
assert.Equal(t, w.Code, 404) assert.Equal(t, http.StatusNotFound, w.Code)
} }
func TestRouterNotFound(t *testing.T) { func TestRouterNotFound(t *testing.T) {
@ -360,45 +370,45 @@ func TestRouterNotFound(t *testing.T) {
code int code int
location string location string
}{ }{
{"/path/", 301, "/path"}, // TSR -/ {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
{"/dir", 301, "/dir/"}, // TSR +/ {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/
{"", 301, "/"}, // TSR +/ {"", http.StatusMovedPermanently, "/"}, // TSR +/
{"/PATH", 301, "/path"}, // Fixed Case {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case
{"/DIR/", 301, "/dir/"}, // Fixed Case {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case
{"/PATH/", 301, "/path"}, // Fixed Case -/ {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
{"/DIR", 301, "/dir/"}, // Fixed Case +/ {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/
{"/../path", 301, "/path"}, // CleanPath {"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath
{"/nope", 404, ""}, // NotFound {"/nope", http.StatusNotFound, ""}, // NotFound
} }
for _, tr := range testRoutes { for _, tr := range testRoutes {
w := performRequest(router, "GET", tr.route) w := performRequest(router, "GET", tr.route)
assert.Equal(t, w.Code, tr.code) assert.Equal(t, tr.code, w.Code)
if w.Code != 404 { if w.Code != http.StatusNotFound {
assert.Equal(t, fmt.Sprint(w.Header().Get("Location")), tr.location) assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
} }
} }
// Test custom not found handler // Test custom not found handler
var notFound bool var notFound bool
router.NoRoute(func(c *Context) { router.NoRoute(func(c *Context) {
c.AbortWithStatus(404) c.AbortWithStatus(http.StatusNotFound)
notFound = true notFound = true
}) })
w := performRequest(router, "GET", "/nope") w := performRequest(router, "GET", "/nope")
assert.Equal(t, w.Code, 404) assert.Equal(t, http.StatusNotFound, w.Code)
assert.True(t, notFound) assert.True(t, notFound)
// Test other method than GET (want 307 instead of 301) // Test other method than GET (want 307 instead of 301)
router.PATCH("/path", func(c *Context) {}) router.PATCH("/path", func(c *Context) {})
w = performRequest(router, "PATCH", "/path/") w = performRequest(router, "PATCH", "/path/")
assert.Equal(t, w.Code, 307) assert.Equal(t, http.StatusTemporaryRedirect, w.Code)
assert.Equal(t, fmt.Sprint(w.Header()), "map[Location:[/path]]") assert.Equal(t, "map[Location:[/path]]", fmt.Sprint(w.Header()))
// Test special case where no node for the prefix "/" exists // Test special case where no node for the prefix "/" exists
router = New() router = New()
router.GET("/a", func(c *Context) {}) router.GET("/a", func(c *Context) {})
w = performRequest(router, "GET", "/") w = performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 404) assert.Equal(t, http.StatusNotFound, w.Code)
} }
func TestRouteRawPath(t *testing.T) { func TestRouteRawPath(t *testing.T) {
@ -409,15 +419,15 @@ func TestRouteRawPath(t *testing.T) {
name := c.Params.ByName("name") name := c.Params.ByName("name")
num := c.Params.ByName("num") num := c.Params.ByName("num")
assert.Equal(t, c.Param("name"), name) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, c.Param("num"), num) assert.Equal(t, num, c.Param("num"))
assert.Equal(t, "Some/Other/Project", name) assert.Equal(t, "Some/Other/Project", name)
assert.Equal(t, "222", num) assert.Equal(t, "222", num)
}) })
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222") w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
} }
func TestRouteRawPathNoUnescape(t *testing.T) { func TestRouteRawPathNoUnescape(t *testing.T) {
@ -429,15 +439,15 @@ func TestRouteRawPathNoUnescape(t *testing.T) {
name := c.Params.ByName("name") name := c.Params.ByName("name")
num := c.Params.ByName("num") num := c.Params.ByName("num")
assert.Equal(t, c.Param("name"), name) assert.Equal(t, name, c.Param("name"))
assert.Equal(t, c.Param("num"), num) assert.Equal(t, num, c.Param("num"))
assert.Equal(t, "Some%2FOther%2FProject", name) assert.Equal(t, "Some%2FOther%2FProject", name)
assert.Equal(t, "333", num) assert.Equal(t, "333", num)
}) })
w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333") w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333")
assert.Equal(t, w.Code, 200) assert.Equal(t, http.StatusOK, w.Code)
} }
func TestRouteServeErrorWithWriteHeader(t *testing.T) { func TestRouteServeErrorWithWriteHeader(t *testing.T) {

View File

@ -4,9 +4,7 @@
package gin package gin
import ( import "net/http"
"net/http"
)
// CreateTestContext returns a fresh engine and context for testing purposes // CreateTestContext returns a fresh engine and context for testing purposes
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) { func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {

18
testdata/certificate/cert.pem vendored Normal file
View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC9DCCAdygAwIBAgIQUNSK+OxWHYYFxHVJV0IlpDANBgkqhkiG9w0BAQsFADAS
MRAwDgYDVQQKEwdBY21lIENvMB4XDTE3MTExNjEyMDA0N1oXDTE4MTExNjEyMDA0
N1owEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAKmyj/YZpD59Bpy4w3qf6VzMw9uUBsWp+IP4kl7z5cmGHYUHn/YopTLH
vR23GAB12p6Km5QWzCBuJF4j61PJXHfg3/rjShZ77JcQ3kzxuy1iKDI+DNKN7Klz
rdjJ49QD0lczZHeBvvCL7JsJFKFjGy62rnStuW8LaIEdtjXT+GUZTxJh6G7yPYfD
MS1IsdMQGOdbGwNa+qogMuQPh0TzHw+C73myKrjY6pREijknMC/rnIMz9dLPt6Kl
xXy4br443dpY6dYGIhDuKhROT+vZ05HKasuuQUFhY7v/KoUpEZMB9rfUSzjQ5fak
eDUAMniXRcd+DmwvboG2TI6ixmuPK+ECAwEAAaNGMEQwDgYDVR0PAQH/BAQDAgWg
MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDwYDVR0RBAgwBocE
fwAAATANBgkqhkiG9w0BAQsFAAOCAQEAMXOLvj7BFsxdbcfRPBd0OFrH/8lI7vPV
LRcJ6r5iv0cnNvZXXbIOQLbg4clJAWjoE08nRm1KvNXhKdns0ELEV86YN2S6jThq
rIGrBqKtaJLB3M9BtDSiQ6SGPLYrWvmhj3Avi8PbSGy51bpGbqopd16j6LYU7Cp2
TefMRlOAFtHojpCVon1CMpqcNxS0WNlQ3lUBSrw3HB0o12x++roja2ibF54tSHXB
KUuadoEzN+mMBwenEBychmAGzdiG4GQHRmhigh85+mtW6UMGiqyCZHs0EgE9FCLL
sRrsTI/VOzLz6lluygXkOsXrP+PP0SvmE3eylWjj9e2nj/u/Cy2YKg==
-----END CERTIFICATE-----

27
testdata/certificate/key.pem vendored Normal file
View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAqbKP9hmkPn0GnLjDep/pXMzD25QGxan4g/iSXvPlyYYdhQef
9iilMse9HbcYAHXanoqblBbMIG4kXiPrU8lcd+Df+uNKFnvslxDeTPG7LWIoMj4M
0o3sqXOt2Mnj1APSVzNkd4G+8IvsmwkUoWMbLraudK25bwtogR22NdP4ZRlPEmHo
bvI9h8MxLUix0xAY51sbA1r6qiAy5A+HRPMfD4LvebIquNjqlESKOScwL+ucgzP1
0s+3oqXFfLhuvjjd2ljp1gYiEO4qFE5P69nTkcpqy65BQWFju/8qhSkRkwH2t9RL
ONDl9qR4NQAyeJdFx34ObC9ugbZMjqLGa48r4QIDAQABAoIBAD5mhd+GMEo2KU9J
9b/Ku8I/HapJtW/L/7Fvn0tBPncrVQGM+zpGWfDhV95sbGwG6lwwNeNvuqIWPlNL
vAY0XkdKrrIQEDdSXH50WnpKzXxzwrou7QIj5Cmvevbjzl4xBZDBOilj0XWczmV4
IljyG5XC4UXQeAaoWEZaSZ1jk8yAt2Zq1Hgg7HqhHsK/arWXBgax+4K5nV/s9gZx
yjKU9mXTIs7k/aNnZqwQKqcZF+l3mvbZttOaFwsP14H0I8OFWhnM9hie54Dejqxi
f4/llNxDqUs6lqJfP3qNxtORLcFe75M+Yl8v7g2hkjtLdZBakPzSTEx3TAK/UHgi
aM8DdxECgYEA3fmg/PI4EgUEj0C3SCmQXR/CnQLMUQgb54s0asp4akvp+M7YCcr1
pQd3HFUpBwhBcJg5LeSe87vLupY7pHCKk56cl9WY6hse0b9sP/7DWJuGiO62m0E0
vNjQ2jpG99oR2ROIHHeWsGCpGLmrRT/kY+vR3M+AOLZniXlOCw8k0aUCgYEAw7WL
XFWLxgZYQYilywqrQmfv1MBfaUCvykO6oWB+f6mmnihSFjecI+nDw/b3yXVYGEgy
0ebkuw0jP8suC8wBqX9WuXj+9nZNomJRssJyOMiEhDEqUiTztFPSp9pdruoakLTh
Wk1p9NralOqGPUmxpXlFKVmYRTUbluikVxDypI0CgYBn6sqEQH0hann0+o4TWWn9
PrYkPUAbm1k8771tVTZERR/W3Dbldr/DL5iCihe39BR2urziEEqdvkglJNntJMar
TzDuIBADYQjvltb9qq4XGFBGYMLaMg+XbUVxNKEuvUdnwa4R7aZ9EfN34MwekkfA
w5Cu9/GGG1ajVEfGA6PwBQKBgA3o71jGs8KFXOx7e90sivOTU5Z5fc6LTHNB0Rf7
NcJ5GmCPWRY/KZfb25AoE4B8GKDRMNt+X69zxZeZJ1KrU0rqxA02rlhyHB54gnoE
G/4xMkn6/JkOC0w70PMhMBtohC7YzFOQwQEoNPT0nkno3Pl33xSLS6lPlwBo1JVj
nPtZAoGACXNLXYkR5vexE+w6FGl59r4RQhu1XU8Mr5DIHeB7kXPN3RKbS201M+Tb
SB5jbu0iDV477XkzSNmhaksFf2wM9MT6CaE+8n3UU5tMa+MmBGgwYTp/i9HkqVh5
jjpJifn1VWBINd4cpNzwCg9LXoo0tbtUPWwGzqVeyo/YE5GIHGo=
-----END RSA PRIVATE KEY-----

View File

@ -3,7 +3,7 @@
// DO NOT EDIT! // 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: It is generated from these files:
test.proto test.proto
@ -11,7 +11,7 @@ It is generated from these files:
It has these top-level messages: It has these top-level messages:
Test Test
*/ */
package example package protoexample
import proto "github.com/golang/protobuf/proto" import proto "github.com/golang/protobuf/proto"
import math "math" import math "math"
@ -109,5 +109,5 @@ func (m *Test_OptionalGroup) GetRequiredField() string {
} }
func init() { 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;}; enum FOO {X=17;};

18
tree.go
View File

@ -87,13 +87,13 @@ const (
type node struct { type node struct {
path string path string
wildChild bool
nType nodeType
maxParams uint8
indices string indices string
children []*node children []*node
handlers HandlersChain handlers HandlersChain
priority uint32 priority uint32
nType nodeType
maxParams uint8
wildChild bool
} }
// increments priority of the given child and reorders if necessary. // increments priority of the given child and reorders if necessary.
@ -232,7 +232,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
} else if i == len(path) { // Make node a (in-path) leaf } else if i == len(path) { // Make node a (in-path) leaf
if n.handlers != nil { if n.handlers != nil {
panic("handlers are already registered for path ''" + fullPath + "'") panic("handlers are already registered for path '" + fullPath + "'")
} }
n.handlers = handlers n.handlers = handlers
} }
@ -247,7 +247,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
var offset int // already handled bytes of the path var offset int // already handled bytes of the path
// find prefix until first wildcard (beginning with ':'' or '*'') // find prefix until first wildcard (beginning with ':' or '*')
for i, max := 0, len(path); numParams > 0; i++ { for i, max := 0, len(path); numParams > 0; i++ {
c := path[i] c := path[i]
if c != ':' && c != '*' { if c != ':' && c != '*' {
@ -384,7 +384,7 @@ walk: // Outer loop for walking the tree
// Nothing found. // Nothing found.
// We can recommend to redirect to the same URL without a // We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path. // trailing slash if a leaf exists for that path.
tsr = (path == "/" && n.handlers != nil) tsr = path == "/" && n.handlers != nil
return return
} }
@ -424,7 +424,7 @@ walk: // Outer loop for walking the tree
} }
// ... but we can't // ... but we can't
tsr = (len(path) == end+1) tsr = len(path) == end+1
return return
} }
@ -435,7 +435,7 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation // trailing slash exists for TSR recommendation
n = n.children[0] n = n.children[0]
tsr = (n.path == "/" && n.handlers != nil) tsr = n.path == "/" && n.handlers != nil
} }
return return
@ -530,7 +530,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPa
// Nothing found. We can recommend to redirect to the same URL // Nothing found. We can recommend to redirect to the same URL
// without a trailing slash if a leaf exists for that path // without a trailing slash if a leaf exists for that path
found = (fixTrailingSlash && path == "/" && n.handlers != nil) found = fixTrailingSlash && path == "/" && n.handlers != nil
return return
} }

View File

@ -5,22 +5,11 @@
package gin package gin
import ( import (
"fmt"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
) )
func printChildren(n *node, prefix string) {
fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handlers, n.wildChild, n.nType)
for l := len(n.path); l > 0; l-- {
prefix += " "
}
for _, child := range n.children {
printChildren(child, prefix)
}
}
// 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 addressses
var fakeHandlerValue string var fakeHandlerValue string

View File

@ -33,18 +33,23 @@ func Bind(val interface{}) HandlerFunc {
} }
} }
// WrapF is a helper function for wrapping http.HandlerFunc
// Returns a Gin middleware
func WrapF(f http.HandlerFunc) HandlerFunc { func WrapF(f http.HandlerFunc) HandlerFunc {
return func(c *Context) { return func(c *Context) {
f(c.Writer, c.Request) f(c.Writer, c.Request)
} }
} }
// WrapH is a helper function for wrapping http.Handler
// Returns a Gin middleware
func WrapH(h http.Handler) HandlerFunc { func WrapH(h http.Handler) HandlerFunc {
return func(c *Context) { return func(c *Context) {
h.ServeHTTP(c.Writer, c.Request) h.ServeHTTP(c.Writer, c.Request)
} }
} }
// H is a shortcut for map[string]interface{}
type H map[string]interface{} type H map[string]interface{}
// MarshalXML allows type H to be used with xml.Marshal. // MarshalXML allows type H to be used with xml.Marshal.
@ -65,10 +70,8 @@ func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return err return err
} }
} }
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return err return e.EncodeToken(xml.EndElement{Name: start.Name})
}
return nil
} }
func assert1(guard bool, text string) { func assert1(guard bool, text string) {
@ -100,10 +103,7 @@ func parseAccept(acceptHeader string) []string {
parts := strings.Split(acceptHeader, ",") parts := strings.Split(acceptHeader, ",")
out := make([]string, 0, len(parts)) out := make([]string, 0, len(parts))
for _, part := range parts { for _, part := range parts {
if index := strings.IndexByte(part, ';'); index >= 0 { if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" {
part = part[0:index]
}
if part = strings.TrimSpace(part); part != "" {
out = append(out, part) out = append(out, part)
} }
} }

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