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
coverage.out
count.out
test

View File

@ -5,10 +5,11 @@ go:
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- master
git:
depth: 3
depth: 10
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.
## gin 1.x series authors
**Gin Core Team:** Bo-Yi Wu (@appleboy), 田欧 (@thinkerou), Javier Provecho (@javierprovecho)
## gin 0.x series authors
**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
**Maintainers:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
People and companies, who have contributed, in alphabetical order.

View File

@ -1,6 +1,28 @@
# CHANGELOG
### Gin 1.2
### Gin 1.3.0
- [NEW] Add [`func (*Context) QueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.QueryMap), [`func (*Context) GetQueryMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetQueryMap), [`func (*Context) PostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.PostFormMap) and [`func (*Context) GetPostFormMap`](https://godoc.org/github.com/gin-gonic/gin#Context.GetPostFormMap) to support `type map[string]string` as query string or form parameters, see [#1383](https://github.com/gin-gonic/gin/pull/1383)
- [NEW] Add [`func (*Context) AsciiJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.AsciiJSON), see [#1358](https://github.com/gin-gonic/gin/pull/1358)
- [NEW] Add `Pusher()` in [`type ResponseWriter`](https://godoc.org/github.com/gin-gonic/gin#ResponseWriter) for supporting http2 push, see [#1273](https://github.com/gin-gonic/gin/pull/1273)
- [NEW] Add [`func (*Context) DataFromReader`](https://godoc.org/github.com/gin-gonic/gin#Context.DataFromReader) for serving dynamic data, see [#1304](https://github.com/gin-gonic/gin/pull/1304)
- [NEW] Add [`func (*Context) ShouldBindBodyWith`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindBodyWith) allowing to call binding multiple times, see [#1341](https://github.com/gin-gonic/gin/pull/1341)
- [NEW] Support pointers in form binding, see [#1336](https://github.com/gin-gonic/gin/pull/1336)
- [NEW] Add [`func (*Context) JSONP`](https://godoc.org/github.com/gin-gonic/gin#Context.JSONP), see [#1333](https://github.com/gin-gonic/gin/pull/1333)
- [NEW] Support default value in form binding, see [#1138](https://github.com/gin-gonic/gin/pull/1138)
- [NEW] Expose validator engine in [`type StructValidator`](https://godoc.org/github.com/gin-gonic/gin/binding#StructValidator), see [#1277](https://github.com/gin-gonic/gin/pull/1277)
- [NEW] Add [`func (*Context) ShouldBind`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBind), [`func (*Context) ShouldBindQuery`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindQuery) and [`func (*Context) ShouldBindJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.ShouldBindJSON), see [#1047](https://github.com/gin-gonic/gin/pull/1047)
- [NEW] Add support for `time.Time` location in form binding, see [#1117](https://github.com/gin-gonic/gin/pull/1117)
- [NEW] Add [`func (*Context) BindQuery`](https://godoc.org/github.com/gin-gonic/gin#Context.BindQuery), see [#1029](https://github.com/gin-gonic/gin/pull/1029)
- [NEW] Make [jsonite](https://github.com/json-iterator/go) optional with build tags, see [#1026](https://github.com/gin-gonic/gin/pull/1026)
- [NEW] Show query string in logger, see [#999](https://github.com/gin-gonic/gin/pull/999)
- [NEW] Add [`func (*Context) SecureJSON`](https://godoc.org/github.com/gin-gonic/gin#Context.SecureJSON), see [#987](https://github.com/gin-gonic/gin/pull/987) and [#993](https://github.com/gin-gonic/gin/pull/993)
- [DEPRECATE] `func (*Context) GetCookie` for [`func (*Context) Cookie`](https://godoc.org/github.com/gin-gonic/gin#Context.Cookie)
- [FIX] Don't display color tags if [`func DisableConsoleColor`](https://godoc.org/github.com/gin-gonic/gin#DisableConsoleColor) called, see [#1072](https://github.com/gin-gonic/gin/pull/1072)
- [FIX] Gin Mode `""` when calling [`func Mode`](https://godoc.org/github.com/gin-gonic/gin#Mode) now returns `const DebugMode`, see [#1250](https://github.com/gin-gonic/gin/pull/1250)
- [FIX] `Flush()` now doesn't overwrite `responseWriter` status code, see [#1460](https://github.com/gin-gonic/gin/pull/1460)
### Gin 1.2.0
- [NEW] Switch from godeps to govendor
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls

View File

@ -1,15 +1,16 @@
GOFMT ?= gofmt "-s"
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/*")
all: build
all: install
install: deps
govendor sync
.PHONY: test
test:
go test -v -covermode=count -coverprofile=coverage.out
sh coverage.sh
.PHONY: fmt
fmt:
@ -26,7 +27,7 @@ fmt-check:
fi;
vet:
go vet $(PACKAGES)
go vet $(VETPACKAGES)
deps:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \

684
README.md
View File

@ -7,11 +7,120 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin)
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
![Gin console logger](https://gin-gonic.github.io/gin/other/console.png)
## Contents
- [Installation](#installation)
- [Prerequisite](#prerequisite)
- [Quick start](#quick-start)
- [Benchmarks](#benchmarks)
- [Gin v1.stable](#gin-v1-stable)
- [Build with jsoniter](#build-with-jsoniter)
- [API Examples](#api-examples)
- [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options)
- [Parameters in path](#parameters-in-path)
- [Querystring parameters](#querystring-parameters)
- [Multipart/Urlencoded Form](#multiparturlencoded-form)
- [Another example: query + post form](#another-example-query--post-form)
- [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
- [Upload files](#upload-files)
- [Grouping routes](#grouping-routes)
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
- [Using middleware](#using-middleware)
- [How to write log file](#how-to-write-log-file)
- [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
# assume the following codes in example.go file
$ cat example.go
@ -87,61 +196,9 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894
- [x] Battle tested
- [x] API frozen, new releases will not break your code.
## Start using it
1. Download and install it:
```sh
$ go get github.com/gin-gonic/gin
```
2. Import it in your code:
```go
import "github.com/gin-gonic/gin"
```
3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
```go
import "net/http"
```
### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor)
1. `go get` govendor
```sh
$ go get github.com/kardianos/govendor
```
2. Create your project folder and `cd` inside
```sh
$ mkdir -p ~/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)
Gin use `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags.
Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags.
```sh
$ go build -tags=jsoniter .
@ -181,7 +238,7 @@ func main() {
func main() {
router := gin.Default()
// This handler will match /user/john but will not match neither /user/ or /user
// This handler will match /user/john but will not match /user/ or /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
@ -268,6 +325,34 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great
```
### Map as querystring or postform parameters
```
POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded
names[first]=thinkerou&names[second]=tianou
```
```go
func main() {
router := gin.Default()
router.POST("/post", func(c *gin.Context) {
ids := c.QueryMap("ids")
names := c.PostFormMap("names")
fmt.Printf("ids: %v; names: %v", ids, names)
})
router.Run(":8080")
}
```
```
ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
```
### Upload files
#### Single file
@ -385,7 +470,7 @@ func main() {
r := gin.New()
// 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
r.Use(gin.Logger())
@ -435,7 +520,7 @@ func main() {
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:
- **Type** - Must bind
- **Methods** - `Bind`, `BindJSON`, `BindQuery`
- **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`
- **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method.
- **Type** - Should bind
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindQuery`
- **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`
- **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately.
When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`.
@ -462,8 +547,8 @@ You can also specify that specific fields are required. If a field is decorated
```go
// Binding from JSON
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
User string `form:"user" json:"user" xml:"user" binding:"required"`
Password string `form:"password" json:"password" xml:"password" binding:"required"`
}
func main() {
@ -472,30 +557,55 @@ func main() {
// Example for binding JSON ({"user": "manu", "password": "123"})
router.POST("/loginJSON", func(c *gin.Context) {
var json Login
if err = c.ShouldBindJSON(&json); err == nil {
if json.User == "manu" && json.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
} else {
if err := c.ShouldBindXML(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if json.User != "manu" || json.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// Example for binding XML (
// <?xml version="1.0" encoding="UTF-8"?>
// <root>
// <user>user</user>
// <password>123</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)
router.POST("/loginForm", func(c *gin.Context) {
var form Login
// This will infer what binder to use depending on the content-type header.
if err := c.ShouldBind(&form); err == nil {
if form.User == "manu" && form.Password == "123" {
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
}
} else {
if err := c.ShouldBind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if form.User != "manu" || form.Password != "123" {
c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
})
// Listen and serve on 0.0.0.0:8080
@ -525,6 +635,10 @@ $ curl -v -X POST \
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
```
**Skip validate**
When running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again.
### Custom Validators
It is also possible to register custom validators. See the [example code](examples/custom-validation/server.go).
@ -540,7 +654,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
validator "gopkg.in/go-playground/validator.v8"
"gopkg.in/go-playground/validator.v8"
)
type Booking struct {
@ -563,7 +677,11 @@ func bookableDate(
func main() {
route := gin.Default()
binding.Validator.RegisterValidation("bookabledate", bookableDate)
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable)
route.Run(":8085")
}
@ -579,13 +697,16 @@ func getBookable(c *gin.Context) {
```
```console
$ curl "localhost:8085/bookable?check_in=2017-08-16&check_out=2017-08-17"
$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
{"message":"Booking dates are valid!"}
$ curl "localhost:8085/bookable?check_in=2017-08-15&check_out=2017-08-16"
$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09"
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}
```
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
See the [struct-lvl-validation example](examples/struct-lvl-validations) to learn more.
### Only Bind Query String
`ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017).
@ -750,7 +871,7 @@ Test it with:
$ curl -v --form user=user --form password=password http://localhost:8080/login
```
### XML, JSON and YAML rendering
### XML, JSON, YAML and ProtoBuf rendering
```go
func main() {
@ -784,6 +905,19 @@ func main() {
c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
})
r.GET("/someProtoBuf", func(c *gin.Context) {
reps := []int64{int64(1), int64(2)}
label := "test"
// The specific definition of protobuf is written in the testdata/protoexample file.
data := &protoexample.Test{
Label: &label,
Reps: reps,
}
// Note that data becomes binary data in the response
// Will output protoexample.Test protobuf serialized data
c.ProtoBuf(http.StatusOK, data)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
@ -811,6 +945,51 @@ func main() {
r.Run(":8080")
}
```
#### JSONP
Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists.
```go
func main() {
r := gin.Default()
r.GET("/JSONP?callback=x", func(c *gin.Context) {
data := map[string]interface{}{
"foo": "bar",
}
//callback is x
// Will output : x({\"foo\":\"bar\"})
c.JSONP(http.StatusOK, data)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
```
#### AsciiJSON
Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters.
```go
func main() {
r := gin.Default()
r.GET("/someJSON", func(c *gin.Context) {
data := map[string]interface{}{
"lang": "GO语言",
"tag": "<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
@ -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
Using LoadHTMLGlob() or LoadHTMLFiles()
@ -978,7 +1183,7 @@ func main() {
router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
router.LoadHTMLFiles("./fixtures/basic/raw.tmpl")
router.LoadHTMLFiles("./testdata/template/raw.tmpl")
router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
@ -1008,14 +1213,26 @@ Gin allow by default use only one html.Template. Check [a multitemplate render](
### Redirects
Issuing a HTTP redirect is easy:
Issuing a HTTP redirect is easy. Both internal and external locations are supported.
```go
r.GET("/test", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
})
```
Both internal and external locations are supported.
Issuing a Router redirect, use `HandleContext` like below.
``` go
r.GET("/test", func(c *gin.Context) {
c.Request.URL.Path = "/test2"
r.HandleContext(c)
})
r.GET("/test2", func(c *gin.Context) {
c.JSON(200, gin.H{"hello": "world"})
})
```
### Custom Middleware
@ -1099,7 +1316,7 @@ func main() {
### 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
func main() {
@ -1161,7 +1378,7 @@ func main() {
example for 1-line LetsEncrypt HTTPS servers.
[embedmd]:# (examples/auto-tls/example1.go go)
[embedmd]:# (examples/auto-tls/example1/main.go go)
```go
package main
@ -1186,7 +1403,7 @@ func main() {
example for custom autocert manager.
[embedmd]:# (examples/auto-tls/example2.go go)
[embedmd]:# (examples/auto-tls/example2/main.go go)
```go
package main
@ -1218,7 +1435,7 @@ func main() {
### 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)
```go
@ -1351,8 +1568,8 @@ func main() {
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil {
log.Printf("listen: %s\n", err)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
@ -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.

15
auth.go
View File

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

View File

@ -22,16 +22,16 @@ func TestBasicAuth(t *testing.T) {
assert.Len(t, pairs, 3)
assert.Contains(t, pairs, authPair{
User: "bar",
Value: "Basic YmFyOmZvbw==",
user: "bar",
value: "Basic YmFyOmZvbw==",
})
assert.Contains(t, pairs, authPair{
User: "foo",
Value: "Basic Zm9vOmJhcg==",
user: "foo",
value: "Basic Zm9vOmJhcg==",
})
assert.Contains(t, pairs, authPair{
User: "admin",
Value: "Basic YWRtaW46cGFzc3dvcmQ=",
user: "admin",
value: "Basic YWRtaW46cGFzc3dvcmQ=",
})
}
@ -93,7 +93,7 @@ func TestBasicAuthSucceed(t *testing.T) {
router := New()
router.Use(BasicAuth(accounts))
router.GET("/login", func(c *Context) {
c.String(200, c.MustGet(AuthUserKey).(string))
c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
})
w := httptest.NewRecorder()
@ -101,7 +101,7 @@ func TestBasicAuthSucceed(t *testing.T) {
req.Header.Set("Authorization", authorizationHeader("admin", "password"))
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "admin", w.Body.String())
}
@ -112,7 +112,7 @@ func TestBasicAuth401(t *testing.T) {
router.Use(BasicAuth(accounts))
router.GET("/login", func(c *Context) {
called = true
c.String(200, c.MustGet(AuthUserKey).(string))
c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
})
w := httptest.NewRecorder()
@ -121,7 +121,7 @@ func TestBasicAuth401(t *testing.T) {
router.ServeHTTP(w, req)
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"))
}
@ -132,7 +132,7 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\""))
router.GET("/login", func(c *Context) {
called = true
c.String(200, c.MustGet(AuthUserKey).(string))
c.String(http.StatusOK, c.MustGet(AuthUserKey).(string))
})
w := httptest.NewRecorder()
@ -141,6 +141,6 @@ func TestBasicAuth401WithCustomRealm(t *testing.T) {
router.ServeHTTP(w, req)
assert.False(t, called)
assert.Equal(t, 401, w.Code)
assert.Equal(t, http.StatusUnauthorized, w.Code)
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"`
}{"ok"}
router.GET("/json", func(c *Context) {
c.JSON(200, data)
c.JSON(http.StatusOK, data)
})
runRequest(B, router, "GET", "/json")
}
var htmlContentType = []string{"text/html; charset=utf-8"}
func BenchmarkOneRouteHTML(B *testing.B) {
router := New()
t := template.Must(template.New("index").Parse(`
@ -68,7 +66,7 @@ func BenchmarkOneRouteHTML(B *testing.B) {
router.SetHTMLTemplate(t)
router.GET("/html", func(c *Context) {
c.HTML(200, "index", "hola")
c.HTML(http.StatusOK, "index", "hola")
})
runRequest(B, router, "GET", "/html")
}
@ -84,7 +82,7 @@ func BenchmarkOneRouteSet(B *testing.B) {
func BenchmarkOneRouteString(B *testing.B) {
router := New()
router.GET("/text", func(c *Context) {
c.String(200, "this is a plain text")
c.String(http.StatusOK, "this is a plain text")
})
runRequest(B, router, "GET", "/text")
}

View File

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

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{}
// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
if kindOfData(obj) == reflect.Struct {
value := reflect.ValueOf(obj)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
if valueType == reflect.Struct {
v.lazyinit()
if err := v.validate.Struct(obj); err != nil {
return error(err)
return err
}
}
return nil
}
func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) error {
// Engine returns the underlying validator engine which powers the default
// Validator instance. This is useful if you want to register custom validations
// or struct level validations. See validator GoDoc for more info -
// https://godoc.org/gopkg.in/go-playground/validator.v8
func (v *defaultValidator) Engine() interface{} {
v.lazyinit()
return v.validate.RegisterValidation(key, fn)
return v.validate
}
func (v *defaultValidator) lazyinit() {
@ -39,12 +49,3 @@ func (v *defaultValidator) lazyinit() {
v.validate = validator.New(config)
})
}
func kindOfData(data interface{}) reflect.Kind {
value := reflect.ValueOf(data)
valueType := value.Kind()
if valueType == reflect.Ptr {
valueType = value.Elem().Kind()
}
return valueType
}

View File

@ -8,6 +8,7 @@ import (
"errors"
"reflect"
"strconv"
"strings"
"time"
)
@ -23,12 +24,28 @@ func mapForm(ptr interface{}, form map[string][]string) error {
structFieldKind := structField.Kind()
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 == "" {
inputFieldName = typeField.Name
// if "form" tag is nil, we inspect if the field is a struct.
// if "form" tag is nil, we inspect if the field is a struct or struct pointer.
// this would not make sense for JSON parsing but it does for a form
// since data is flatten
if structFieldKind == reflect.Ptr {
if !structField.Elem().IsValid() {
structField.Set(reflect.New(structField.Type().Elem()))
}
structField = structField.Elem()
structFieldKind = structField.Kind()
}
if structFieldKind == reflect.Struct {
err := mapForm(structField.Addr().Interface(), form)
if err != nil {
@ -38,9 +55,14 @@ func mapForm(ptr interface{}, form map[string][]string) error {
}
}
inputValue, exists := form[inputFieldName]
if !exists {
if defaultValue == "" {
continue
}
inputValue = make([]string, 1)
inputValue[0] = defaultValue
}
numElems := len(inputValue)
if structFieldKind == reflect.Slice && numElems > 0 {
@ -97,6 +119,12 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V
return setFloatField(val, 64, structField)
case reflect.String:
structField.SetString(val)
case reflect.Ptr:
if !structField.Elem().IsValid() {
structField.Set(reflect.New(structField.Type().Elem()))
}
structFieldElem := structField.Elem()
return setWithProperType(structFieldElem.Kind(), val, structFieldElem)
default:
return errors.New("Unknown type")
}
@ -133,7 +161,7 @@ func setBoolField(val string, field reflect.Value) error {
if err == nil {
field.SetBool(boolVal)
}
return nil
return err
}
func setFloatField(val string, bitSize int, field reflect.Value) error {
@ -150,7 +178,7 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
timeFormat := structField.Tag.Get("time_format")
if timeFormat == "" {
return errors.New("Blank time format")
timeFormat = time.RFC3339
}
if val == "" {
@ -179,12 +207,3 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
value.Set(reflect.ValueOf(t))
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
import (
"bytes"
"io"
"net/http"
"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
type jsonBinding struct{}
@ -19,7 +24,15 @@ func (jsonBinding) Name() string {
}
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 {
decoder.UseNumber()
}

View File

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

View File

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

View File

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

View File

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

View File

@ -31,11 +31,10 @@ const (
MIMEPlain = binding.MIMEPlain
MIMEPOSTForm = binding.MIMEPOSTForm
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
)
const (
abortIndex int8 = math.MaxInt8 / 2
)
const abortIndex int8 = math.MaxInt8 / 2
// 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.
@ -105,8 +104,7 @@ func (c *Context) Handler() HandlerFunc {
// See example in GitHub.
func (c *Context) Next() {
c.index++
s := int8(len(c.handlers))
for ; c.index < s; c.index++ {
for s := int8(len(c.handlers)); c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
@ -161,16 +159,15 @@ func (c *Context) Error(err error) *Error {
if err == nil {
panic("err is nil")
}
var parsedError *Error
switch err.(type) {
case *Error:
parsedError = err.(*Error)
default:
parsedError, ok := err.(*Error)
if !ok {
parsedError = &Error{
Err: err,
Type: ErrorTypePrivate,
}
}
c.Errors = append(c.Errors, parsedError)
return parsedError
}
@ -363,6 +360,18 @@ func (c *Context) GetQueryArray(key string) ([]string, bool) {
return []string{}, false
}
// QueryMap returns a map for a given query key.
func (c *Context) QueryMap(key string) map[string]string {
dicts, _ := c.GetQueryMap(key)
return dicts
}
// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
return c.get(c.Request.URL.Query(), key)
}
// PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) string {
@ -418,6 +427,42 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false
}
// PostFormMap returns a map for a given form key.
func (c *Context) PostFormMap(key string) map[string]string {
dicts, _ := c.GetPostFormMap(key)
return dicts
}
// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
req := c.Request
req.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.
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
_, 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:
// "application/json" --> JSON 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 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 {
b := binding.Default(c.Request.Method, c.ContentType())
return c.MustBindWith(obj, b)
@ -466,6 +511,11 @@ func (c *Context) BindJSON(obj interface{}) error {
return c.MustBindWith(obj, binding.JSON)
}
// BindXML is a shortcut for c.MustBindWith(obj, binding.BindXML).
func (c *Context) BindXML(obj interface{}) error {
return c.MustBindWith(obj, binding.XML)
}
// BindQuery is a shortcut for c.MustBindWith(obj, binding.Query).
func (c *Context) BindQuery(obj interface{}) error {
return c.MustBindWith(obj, binding.Query)
@ -476,7 +526,7 @@ func (c *Context) BindQuery(obj interface{}) error {
// See the binding package.
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
if err = c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(400, err).SetType(ErrorTypeBind)
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
}
return
@ -500,6 +550,11 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
return c.ShouldBindWith(obj, binding.JSON)
}
// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
func (c *Context) ShouldBindXML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.XML)
}
// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Query)
@ -511,21 +566,41 @@ func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
// ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request
// body into the context, and reuse when it is called again.
//
// NOTE: This method reads the body before binding. So you should use
// ShouldBindWith for better performance if you need to call only once.
func (c *Context) ShouldBindBodyWith(
obj interface{}, bb binding.BindingBody,
) (err error) {
var body []byte
if cb, ok := c.Get(BodyBytesKey); ok {
if cbb, ok := cb.([]byte); ok {
body = cbb
}
}
if body == nil {
body, err = ioutil.ReadAll(c.Request.Body)
if err != nil {
return err
}
c.Set(BodyBytesKey, body)
}
return bb.BindBody(body, obj)
}
// ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP {
clientIP := c.requestHeader("X-Forwarded-For")
if index := strings.IndexByte(clientIP, ','); index >= 0 {
clientIP = clientIP[0:index]
}
clientIP = strings.TrimSpace(clientIP)
if len(clientIP) > 0 {
return clientIP
}
clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
if clientIP == "" {
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
if len(clientIP) > 0 {
}
if clientIP != "" {
return clientIP
}
}
@ -571,9 +646,9 @@ func bodyAllowedForStatus(status int) bool {
switch {
case status >= 100 && status <= 199:
return false
case status == 204:
case status == http.StatusNoContent:
return false
case status == 304:
case status == http.StatusNotModified:
return false
}
return true
@ -588,7 +663,7 @@ func (c *Context) Status(code int) {
// It writes a header in the response.
// If value == "", this method removes the header `c.Writer.Header().Del(key)`
func (c *Context) Header(key, value string) {
if len(value) == 0 {
if value == "" {
c.Writer.Header().Del(key)
} else {
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})
}
// 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.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj})
}
// AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string.
// It also sets the Content-Type as "application/json".
func (c *Context) AsciiJSON(code int, obj interface{}) {
c.Render(code, render.AsciiJSON{Data: obj})
}
// XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
@ -690,6 +783,11 @@ func (c *Context) YAML(code int, obj interface{}) {
c.Render(code, render.YAML{Data: obj})
}
// ProtoBuf serializes the given struct as ProtoBuf into the response body.
func (c *Context) ProtoBuf(code int, obj interface{}) {
c.Render(code, render.ProtoBuf{Data: obj})
}
// String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) {
c.Render(code, render.String{Format: format, Data: values})
@ -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.
func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath)
@ -801,18 +909,33 @@ func (c *Context) SetAccepted(formats ...string) {
/***** GOLANG.ORG/X/NET/CONTEXT *****/
/************************************/
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
func (c *Context) Deadline() (deadline time.Time, ok bool) {
return
}
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
func (c *Context) Done() <-chan struct{} {
return nil
}
// Err returns a non-nil error value after Done is closed,
// successive calls to Err return the same error.
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
func (c *Context) Err() error {
return nil
}
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
func (c *Context) Value(key interface{}) interface{} {
if key == 0 {
return c.Request

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.
// Use SetMode(gin.Release) to disable debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode.
func IsDebugging() bool {
return ginMode == debugCode
}
@ -47,6 +47,9 @@ func debugPrint(format string, values ...interface{}) {
}
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.
`)

View File

@ -72,7 +72,7 @@ func TestDebugPrintLoadTemplate(t *testing.T) {
setup(&w)
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)
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()
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) {

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
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 {
fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta)
}

View File

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

View File

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

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
import (
"net/http"
"github.com/gin-gonic/gin"
)
var DB = make(map[string]string)
func main() {
func setupRouter() *gin.Engine {
// Disable Console Color
// gin.DisableConsoleColor()
r := gin.Default()
// Ping test
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
c.String(http.StatusOK, "pong")
})
// Get user value
@ -21,9 +23,9 @@ func main() {
user := c.Params.ByName("name")
value, ok := DB[user]
if ok {
c.JSON(200, gin.H{"user": user, "value": value})
c.JSON(http.StatusOK, gin.H{"user": user, "value": value})
} else {
c.JSON(200, gin.H{"user": user, "status": "no value"})
c.JSON(http.StatusOK, gin.H{"user": user, "status": "no value"})
}
})
@ -49,10 +51,15 @@ func main() {
if c.Bind(&json) == nil {
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
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/binding"
validator "gopkg.in/go-playground/validator.v8"
"gopkg.in/go-playground/validator.v8"
)
type Booking struct {
@ -30,7 +30,11 @@ func bookableDate(
func main() {
route := gin.Default()
binding.Validator.RegisterValidation("bookabledate", bookableDate)
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable)
route.Run(":8085")
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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{
"formatAsDate": formatAsDate,
})
router.LoadHTMLFiles("../../fixtures/basic/raw.tmpl")
router.LoadHTMLFiles("../../testdata/template/raw.tmpl")
router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{

79
gin.go
View File

@ -16,7 +16,7 @@ import (
const (
// Version is Framework's version.
Version = "v1.2"
Version = "v1.3.0"
defaultMultipartMemory = 32 << 20 // 32 MB
)
@ -49,16 +49,6 @@ type RoutesInfo []RouteInfo
// Create an instance of Engine, by using New() or Default()
type Engine struct {
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
// 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
// method call.
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{}
@ -159,25 +160,30 @@ func (engine *Engine) Delims(left, right string) *Engine {
return engine
}
// SecureJsonPrefix sets the secureJsonPrefix used in Context.SecureJSON.
func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
engine.secureJsonPrefix = prefix
return engine
}
// LoadHTMLGlob loads HTML files identified by glob pattern
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLGlob(pattern string) {
left := engine.delims.Left
right := engine.delims.Right
templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
if IsDebugging() {
debugPrintLoadTemplate(template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern)))
debugPrintLoadTemplate(templ)
engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
return
}
templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
engine.SetHTMLTemplate(templ)
}
// LoadHTMLFiles loads a slice of HTML files
// and associates the result with HTML renderer.
func (engine *Engine) LoadHTMLFiles(files ...string) {
if IsDebugging() {
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)
}
// SetHTMLTemplate associate a template with HTML renderer.
func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 {
debugPrintWARNINGSetHTMLTemplate()
@ -196,6 +203,7 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)}
}
// SetFuncMap sets the FuncMap used for template.FuncMap.
func (engine *Engine) SetFuncMap(funcMap template.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.
// This can be done by setting c.Request.Path to your new target.
// This can be done by setting c.Request.URL.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
c.reset()
@ -341,7 +349,9 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method == httpMethod {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
handlers, params, tsr := root.getValue(path, c.Params, unescape)
@ -363,21 +373,21 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
}
break
}
}
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method != httpMethod {
if tree.method == httpMethod {
continue
}
if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
c.handlers = engine.allNoMethod
serveError(c, 405, default405Body)
serveError(c, http.StatusMethodNotAllowed, default405Body)
return
}
}
}
}
c.handlers = engine.allNoRoute
serveError(c, 404, default404Body)
serveError(c, http.StatusNotFound, default404Body)
}
var mimePlain = []string{MIMEPlain}
@ -385,28 +395,29 @@ var mimePlain = []string{MIMEPlain}
func serveError(c *Context, code int, defaultMessage []byte) {
c.writermem.status = code
c.Next()
if !c.writermem.Written() {
if c.writermem.Written() {
return
}
if c.writermem.Status() == code {
c.writermem.Header()["Content-Type"] = mimePlain
c.Writer.Write(defaultMessage)
} else {
return
}
c.writermem.WriteHeaderNow()
}
}
return
}
func redirectTrailingSlash(c *Context) {
req := c.Request
path := req.URL.Path
code := 301 // Permanent redirect, request with GET method
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
code = 307
code = http.StatusTemporaryRedirect
}
if len(path) > 1 && path[len(path)-1] == '/' {
req.URL.Path = path[:len(path)-1]
} else {
req.URL.Path = path + "/"
if length := len(path); length > 1 && path[length-1] == '/' {
req.URL.Path = path[:length-1]
}
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
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
path := req.URL.Path
fixedPath, found := root.findCaseInsensitivePath(
cleanPath(path),
trailingSlash,
)
if found {
code := 301 // Permanent redirect, request with GET method
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(path), trailingSlash); ok {
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
code = 307
code = http.StatusTemporaryRedirect
}
req.URL.Path = string(fixedPath)
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())

View File

@ -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.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens.
func RunTLS(addr string, certFile string, keyFile string) (err error) {
func RunTLS(addr, certFile, keyFile string) (err error) {
return engine().RunTLS(addr, certFile, keyFile)
}

View File

@ -94,7 +94,7 @@ func TestUnixSocket(t *testing.T) {
c, err := net.Dial("unix", "/tmp/unix_unit_test")
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)
var response string
for scanner.Scan() {

View File

@ -5,6 +5,7 @@
package gin
import (
"crypto/tls"
"fmt"
"html/template"
"io/ioutil"
@ -21,15 +22,15 @@ func formatAsDate(t time.Time) string {
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() {
SetMode(TestMode)
SetMode(mode)
router := New()
router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{
"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) {
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),
})
})
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")
time.Sleep(1 * time.Second)
return func() {}
}
func setupHTMLGlob(t *testing.T) func() {
func setupHTMLGlob(t *testing.T, mode string, tls bool) func() {
go func() {
SetMode(DebugMode)
SetMode(mode)
router := New()
router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
router.LoadHTMLGlob("./fixtures/basic/*")
router.LoadHTMLGlob("./testdata/template/*")
router.GET("/test", func(c *Context) {
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),
})
})
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")
time.Sleep(1 * time.Second)
return func() {}
}
//TODO
func TestLoadHTMLGlob(t *testing.T) {
td := setupHTMLGlob(t)
td := setupHTMLGlob(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
@ -83,9 +93,55 @@ func TestLoadHTMLGlob(t *testing.T) {
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) {
time.Now()
td := setupHTMLGlob(t)
td := setupHTMLGlob(t, DebugMode, false)
res, err := http.Get("http://127.0.0.1:8888/raw")
if err != nil {
fmt.Println(err)
@ -97,9 +153,6 @@ func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
td()
}
// func (engine *Engine) LoadHTMLFiles(files ...string) {
// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
func init() {
SetMode(TestMode)
}
@ -117,17 +170,17 @@ func TestCreateEngine(t *testing.T) {
// router.LoadHTMLGlob("*.testtmpl")
// r := router.HTMLRender.(render.HTMLDebug)
// 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")
// r = router.HTMLRender.(render.HTMLDebug)
// 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)
// }
func TestLoadHTMLFiles(t *testing.T) {
td := setupHTMLFiles(t)
td := setupHTMLFiles(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/test")
if err != nil {
fmt.Println(err)
@ -138,9 +191,52 @@ func TestLoadHTMLFiles(t *testing.T) {
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) {
time.Now()
td := setupHTMLFiles(t)
td := setupHTMLFiles(t, TestMode, false)
res, err := http.Get("http://127.0.0.1:8888/raw")
if err != nil {
fmt.Println(err)
@ -152,10 +248,6 @@ func TestLoadHTMLFilesFuncMap(t *testing.T) {
td()
}
func TestLoadHTMLReleaseMode(t *testing.T) {
}
func TestAddRoute(t *testing.T) {
router := New()
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

15
mode.go
View File

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

View File

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

View File

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

View File

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

View File

@ -10,8 +10,10 @@ import (
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"runtime"
"time"
)
var (
@ -38,9 +40,9 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
if logger != nil {
stack := stack(3)
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()
@ -107,3 +109,8 @@ func function(pc uintptr) []byte {
name = bytes.Replace(name, centerDot, dot, -1)
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 (
"bytes"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
@ -22,7 +23,7 @@ func TestPanicInHandler(t *testing.T) {
// RUN
w := performRequest(router, "GET", "/recovery")
// 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(), "Oupps, Houston, we have a problem")
assert.Contains(t, buffer.String(), "TestPanicInHandler")
@ -33,11 +34,31 @@ func TestPanicWithAbort(t *testing.T) {
router := New()
router.Use(RecoveryWithWriter(nil))
router.GET("/recovery", func(c *Context) {
c.AbortWithStatus(400)
c.AbortWithStatus(http.StatusBadRequest)
panic("Oupps, Houston, we have a problem")
})
// RUN
w := performRequest(router, "GET", "/recovery")
// TEST
assert.Equal(t, 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 (
"bytes"
"fmt"
"html/template"
"net/http"
"github.com/gin-gonic/gin/json"
@ -24,9 +26,20 @@ type SecureJSON struct {
Data interface{}
}
type JsonpJSON struct {
Callback string
Data interface{}
}
type AsciiJSON struct {
Data interface{}
}
type SecureJSONPrefix string
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) {
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) {
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 {
writeContentType(w, msgpackContentType)
var h codec.Handle = new(codec.MsgpackHandle)
return codec.NewEncoder(w, h).Encode(obj)
var mh codec.MsgpackHandle
return codec.NewEncoder(w, &mh).Encode(obj)
}

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 {
// todo(thinkerou): go1.6 not support StatusPermanentRedirect(308)
// when we upgrade go version we can use http.StatusPermanentRedirect
if (r.Code < 300 || r.Code > 308) && r.Code != 201 {
panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
}

View File

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

View File

@ -7,12 +7,19 @@ package render
import (
"bytes"
"encoding/xml"
"errors"
"html/template"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
)
// TODO unit tests
@ -24,6 +31,9 @@ func TestRenderMsgPack(t *testing.T) {
"foo": "bar",
}
(MsgPack{data}).WriteContentType(w)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
err := (MsgPack{data}).Render(w)
assert.NoError(t, err)
@ -36,7 +46,7 @@ func TestRenderMsgPack(t *testing.T) {
assert.NoError(t, err)
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) {
@ -46,6 +56,9 @@ func TestRenderJSON(t *testing.T) {
"html": "<b>",
}
(JSON{data}).WriteContentType(w)
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
err := (JSON{data}).Render(w)
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"))
}
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) {
w := httptest.NewRecorder()
data := map[string]interface{}{
@ -64,8 +85,17 @@ func TestRenderIndentedJSON(t *testing.T) {
err := (IndentedJSON{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}")
assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
assert.Equal(t, "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}", w.Body.String())
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) {
@ -74,6 +104,9 @@ func TestRenderSecureJSON(t *testing.T) {
"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)
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"))
}
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{}
// 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
}
}
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return err
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
return nil
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,
}
(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) {
@ -125,15 +305,41 @@ func TestRenderXML(t *testing.T) {
"foo": "bar",
}
(XML{data}).WriteContentType(w)
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
err := (XML{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "<map><foo>bar</foo></map>")
assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8")
assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
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) {
@ -146,21 +352,40 @@ func TestRenderData(t *testing.T) {
}).Render(w)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "#!PNG some raw data")
assert.Equal(t, w.Header().Get("Content-Type"), "image/png")
assert.Equal(t, "#!PNG some raw data", w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
}
func TestRenderString(t *testing.T) {
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{
Format: "hola %s %d",
Data: []interface{}{"manu", 2},
}).Render(w)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "hola manu 2")
assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8")
assert.Equal(t, "hola manu 2", w.Body.String())
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) {
@ -175,6 +400,88 @@ func TestRenderHTMLTemplate(t *testing.T) {
err := instance.Render(w)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "Hello alexandernyquist")
assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8")
assert.Equal(t, "Hello alexandernyquist", w.Body.String())
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)
if len(data) > 0 {
fmt.Fprintf(w, format, data...)
} else {
return
}
io.WriteString(w, format)
}
}

View File

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

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)
assert.Equal(t, -1, writer.size)
assert.Equal(t, 200, writer.status)
assert.Equal(t, http.StatusOK, writer.status)
assert.Equal(t, testWritter, writer.ResponseWriter)
assert.Equal(t, -1, w.Size())
assert.Equal(t, 200, w.Status())
assert.Equal(t, http.StatusOK, w.Status())
assert.False(t, w.Written())
}
@ -48,13 +48,13 @@ func TestResponseWriterWriteHeader(t *testing.T) {
writer.reset(testWritter)
w := ResponseWriter(writer)
w.WriteHeader(300)
w.WriteHeader(http.StatusMultipleChoices)
assert.False(t, w.Written())
assert.Equal(t, 300, w.Status())
assert.NotEqual(t, testWritter.Code, 300)
assert.Equal(t, http.StatusMultipleChoices, w.Status())
assert.NotEqual(t, http.StatusMultipleChoices, testWritter.Code)
w.WriteHeader(-1)
assert.Equal(t, 300, w.Status())
assert.Equal(t, http.StatusMultipleChoices, w.Status())
}
func TestResponseWriterWriteHeadersNow(t *testing.T) {
@ -63,12 +63,12 @@ func TestResponseWriterWriteHeadersNow(t *testing.T) {
writer.reset(testWritter)
w := ResponseWriter(writer)
w.WriteHeader(300)
w.WriteHeader(http.StatusMultipleChoices)
w.WriteHeaderNow()
assert.True(t, w.Written())
assert.Equal(t, 0, w.Size())
assert.Equal(t, 300, testWritter.Code)
assert.Equal(t, http.StatusMultipleChoices, testWritter.Code)
writer.size = 10
w.WriteHeaderNow()
@ -84,8 +84,8 @@ func TestResponseWriterWrite(t *testing.T) {
n, err := w.Write([]byte("hola"))
assert.Equal(t, 4, n)
assert.Equal(t, 4, w.Size())
assert.Equal(t, 200, w.Status())
assert.Equal(t, 200, testWritter.Code)
assert.Equal(t, http.StatusOK, w.Status())
assert.Equal(t, http.StatusOK, testWritter.Code)
assert.Equal(t, "hola", testWritter.Body.String())
assert.NoError(t, err)
@ -113,3 +113,19 @@ func TestResponseWriterHijack(t *testing.T) {
w.Flush()
}
func TestResponseWriterFlush(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := &responseWriter{}
writer.reset(w)
writer.WriteHeader(http.StatusInternalServerError)
writer.Flush()
}))
defer testServer.Close()
// should return 500
resp, err := http.Get(testServer.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
}

View File

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

View File

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

View File

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

View File

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

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!
/*
Package example is a generated protocol buffer package.
Package protoexample is a generated protocol buffer package.
It is generated from these files:
test.proto
@ -11,7 +11,7 @@ It is generated from these files:
It has these top-level messages:
Test
*/
package example
package protoexample
import proto "github.com/golang/protobuf/proto"
import math "math"
@ -109,5 +109,5 @@ func (m *Test_OptionalGroup) GetRequiredField() string {
}
func init() {
proto.RegisterEnum("example.FOO", FOO_name, FOO_value)
proto.RegisterEnum("protoexample.FOO", FOO_name, FOO_value)
}

View File

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

18
tree.go
View File

@ -87,13 +87,13 @@ const (
type node struct {
path string
wildChild bool
nType nodeType
maxParams uint8
indices string
children []*node
handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
}
// 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
if n.handlers != nil {
panic("handlers are already registered for path ''" + fullPath + "'")
panic("handlers are already registered for path '" + fullPath + "'")
}
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) {
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++ {
c := path[i]
if c != ':' && c != '*' {
@ -384,7 +384,7 @@ walk: // Outer loop for walking the tree
// Nothing found.
// We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path.
tsr = (path == "/" && n.handlers != nil)
tsr = path == "/" && n.handlers != nil
return
}
@ -424,7 +424,7 @@ walk: // Outer loop for walking the tree
}
// ... but we can't
tsr = (len(path) == end+1)
tsr = len(path) == end+1
return
}
@ -435,7 +435,7 @@ walk: // Outer loop for walking the tree
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
tsr = (n.path == "/" && n.handlers != nil)
tsr = n.path == "/" && n.handlers != nil
}
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
// without a trailing slash if a leaf exists for that path
found = (fixTrailingSlash && path == "/" && n.handlers != nil)
found = fixTrailingSlash && path == "/" && n.handlers != nil
return
}

View File

@ -5,22 +5,11 @@
package gin
import (
"fmt"
"reflect"
"strings"
"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
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 {
return func(c *Context) {
f(c.Writer, c.Request)
}
}
// WrapH is a helper function for wrapping http.Handler
// Returns a Gin middleware
func WrapH(h http.Handler) HandlerFunc {
return func(c *Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}
// H is a shortcut for map[string]interface{}
type H map[string]interface{}
// 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
}
}
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return err
}
return nil
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
func assert1(guard bool, text string) {
@ -100,10 +103,7 @@ func parseAccept(acceptHeader string) []string {
parts := strings.Split(acceptHeader, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
if index := strings.IndexByte(part, ';'); index >= 0 {
part = part[0:index]
}
if part = strings.TrimSpace(part); part != "" {
if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" {
out = append(out, part)
}
}

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