Merge branch 'master' into master

This commit is contained in:
Bo-Yi Wu 2017-07-05 09:11:47 -05:00 committed by GitHub
commit 26ef4e917f
73 changed files with 2418 additions and 477 deletions

4
.gitignore vendored
View File

@ -1,4 +1,4 @@
Godeps/*
!Godeps/Godeps.json
vendor/*
!vendor/vendor.json
coverage.out
count.out

View File

@ -1,14 +1,23 @@
language: go
sudo: false
go:
- 1.4
- 1.5.4
- 1.6.4
- 1.7.4
- tip
- 1.6.x
- 1.7.x
- 1.8.x
- master
git:
depth: 3
install:
- make install
script:
- go test -v -covermode=count -coverprofile=coverage.out
- make vet
- make fmt-check
- make embedmd
- make misspell-check
- make test
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@ -1,8 +1,6 @@
List of all the awesome people working to make Gin the best Web Framework in Go.
##gin 0.x series authors
## gin 0.x series authors
**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
@ -226,4 +224,4 @@ People and companies, who have contributed, in alphabetical order.
**@yuyabee**
- Fixed README
- Fixed README

View File

@ -295,4 +295,4 @@ BenchmarkPossum_GPlusAll 100000 19685 ns/op 6240 B/op
BenchmarkR2router_GPlusAll 100000 16251 ns/op 5040 B/op 76 allocs/op
BenchmarkRevel_GPlusAll 20000 93489 ns/op 21656 B/op 368 allocs/op
BenchmarkRivet_GPlusAll 100000 16907 ns/op 5408 B/op 64 allocs/op
```
```

View File

@ -1,6 +1,47 @@
#CHANGELOG
# CHANGELOG
###Gin 1.0rc2 (...)
### Gin 1.2
- [NEW] Switch from godeps to govendor
- [NEW] Add support for Let's Encrypt via gin-gonic/autotls
- [NEW] Improve README examples and add extra at examples folder
- [NEW] Improved support with App Engine
- [NEW] Add custom template delimiters, see #860
- [NEW] Add Template Func Maps, see #962
- [NEW] Add \*context.Handler(), see #928
- [NEW] Add \*context.GetRawData()
- [NEW] Add \*context.GetHeader() (request)
- [NEW] Add \*context.AbortWithStatusJSON() (JSON content type)
- [NEW] Add \*context.Keys type cast helpers
- [NEW] Add \*context.ShouldBindWith()
- [NEW] Add \*context.MustBindWith()
- [NEW] Add \*engine.SetFuncMap()
- [DEPRECATE] On next release: \*context.BindWith(), see #855
- [FIX] Refactor render
- [FIX] Reworked tests
- [FIX] logger now supports cygwin
- [FIX] Use X-Forwarded-For before X-Real-Ip
- [FIX] time.Time binding (#904)
### Gin 1.1.4
- [NEW] Support google appengine for IsTerminal func
### Gin 1.1.3
- [FIX] Reverted Logger: skip ANSI color commands
### Gin 1.1
- [NEW] Implement QueryArray and PostArray methods
- [NEW] Refactor GetQuery and GetPostForm
- [NEW] Add contribution guide
- [FIX] Corrected typos in README
- [FIX] Removed additional Iota
- [FIX] Changed imports to gopkg instead of github in README (#733)
- [FIX] Logger: skip ANSI color commands if output is not a tty
### Gin 1.0rc2 (...)
- [PERFORMANCE] Fast path for writing Content-Type.
- [PERFORMANCE] Much faster 404 routing
@ -35,7 +76,7 @@
- [FIX] MIT license in every file
###Gin 1.0rc1 (May 22, 2015)
### Gin 1.0rc1 (May 22, 2015)
- [PERFORMANCE] Zero allocation router
- [PERFORMANCE] Faster JSON, XML and text rendering
@ -79,7 +120,7 @@
- [FIX] Better support for Google App Engine (using log instead of fmt)
###Gin 0.6 (Mar 9, 2015)
### Gin 0.6 (Mar 9, 2015)
- [NEW] Support multipart/form-data
- [NEW] NoMethod handler
@ -89,14 +130,14 @@
- [FIX] Improve color logger
###Gin 0.5 (Feb 7, 2015)
### Gin 0.5 (Feb 7, 2015)
- [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding
###Gin 0.4 (Aug 21, 2014)
### Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode
- [NEW] Unit tests
@ -105,7 +146,7 @@
- [FIX] Improved documentation for model binding
###Gin 0.3 (Jul 18, 2014)
### Gin 0.3 (Jul 18, 2014)
- [PERFORMANCE] Normal log and error log are printed in the same call.
- [PERFORMANCE] Improve performance of NoRouter()
@ -123,7 +164,7 @@
- [FIX] Check application/x-www-form-urlencoded when parsing form
###Gin 0.2b (Jul 08, 2014)
### Gin 0.2b (Jul 08, 2014)
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
- [NEW] Travis CI integration
- [NEW] Completely new logger

13
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,13 @@
## Contributing
- 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.
- 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.

36
Godeps/Godeps.json generated
View File

@ -1,36 +0,0 @@
{
"ImportPath": "github.com/gin-gonic/gin",
"GoVersion": "go1.5.1",
"Deps": [
{
"ImportPath": "github.com/davecgh/go-spew/spew",
"Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
},
{
"ImportPath": "github.com/golang/protobuf/proto",
"Rev": "2402d76f3d41f928c7902a765dfc872356dd3aad"
},
{
"ImportPath": "github.com/manucorporat/sse",
"Rev": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d"
},
{
"ImportPath": "github.com/pmezard/go-difflib/difflib",
"Rev": "792786c7400a136282c1664665ae0a8db921c6c2"
},
{
"ImportPath": "github.com/stretchr/testify/assert",
"Comment": "v1.1.3",
"Rev": "f390dcf405f7b83c997eac1b06768bb9f44dec18"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "f315505cf3349909cdf013ea56690da34e96a451"
},
{
"ImportPath": "gopkg.in/go-playground/validator.v8",
"Comment": "v8.15.1",
"Rev": "c193cecd124b5cc722d7ee5538e945bdb3348435"
}
]
}

61
Makefile Normal file
View File

@ -0,0 +1,61 @@
GOFMT ?= gofmt "-s"
PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
all: build
install: deps
govendor sync
.PHONY: test
test:
go test -v -covermode=count -coverprofile=coverage.out
.PHONY: fmt
fmt:
$(GOFMT) -w $(GOFILES)
.PHONY: fmt-check
fmt-check:
# get all go files and run go fmt on them
@diff=$$($(GOFMT) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \
echo "$${diff}"; \
exit 1; \
fi;
vet:
go vet $(PACKAGES)
deps:
@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/kardianos/govendor; \
fi
@hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/campoy/embedmd; \
fi
embedmd:
embedmd -d *.md
.PHONY: lint
lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/golang/lint/golint; \
fi
for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
.PHONY: misspell-check
misspell-check:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/client9/misspell/cmd/misspell; \
fi
misspell -error $(GOFILES)
.PHONY: misspell
misspell:
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
go get -u github.com/client9/misspell/cmd/misspell; \
fi
misspell -w $(GOFILES)

401
README.md
View File

@ -1,24 +1,26 @@
# Gin Web Framework
#Gin Web Framework
<img align="right" src="https://raw.githubusercontent.com/gin-gonic/gin/master/logo.png">
<img align="right" src="https://raw.githubusercontent.com/gin-gonic/gin/master/logo.jpg">
[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin)
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin)
[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin)
[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin)
[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
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)
```sh
$ cat test.go
# assume the following codes in example.go file
$ cat example.go
```
```go
package main
import "gopkg.in/gin-gonic/gin.v1"
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
@ -31,6 +33,11 @@ func main() {
}
```
```
# run example.go and visit 0.0.0.0:8080/ping on browser
$ go run example.go
```
## Benchmarks
Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter)
@ -86,28 +93,63 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648
1. Download and install it:
```sh
$ go get gopkg.in/gin-gonic/gin.v1
```
```sh
$ go get github.com/gin-gonic/gin
```
2. Import it in your code:
```go
import "gopkg.in/gin-gonic/gin.v1"
```
```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"
```
```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
```
## API Examples
#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
```go
func main() {
// Disable Console Color
// gin.DisableConsoleColor()
// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()
@ -127,7 +169,7 @@ func main() {
}
```
#### Parameters in path
### Parameters in path
```go
func main() {
@ -152,7 +194,8 @@ func main() {
}
```
#### Querystring parameters
### Querystring parameters
```go
func main() {
router := gin.Default()
@ -219,34 +262,66 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great
```
### Another example: upload file
### Upload files
References issue [#548](https://github.com/gin-gonic/gin/issues/548).
#### Single file
References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
```go
func main() {
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
// single file
file, _ := c.FormFile("file")
log.Println(file.Filename)
file, header , err := c.Request.FormFile("upload")
filename := header.Filename
fmt.Println(header.Filename)
out, err := os.Create("./tmp/"+filename+".png")
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
c.String(http.StatusOK, fmt.Printf("'%s' uploaded!", file.Filename))
})
router.Run(":8080")
}
```
#### Grouping routes
How to `curl`:
```bash
curl -X POST http://localhost:8080/upload \
-F "file=@/Users/appleboy/test.zip" \
-H "Content-Type: multipart/form-data"
```
#### Multiple files
See the detail [example code](examples/upload-file/multiple).
```go
func main() {
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["upload[]"]
for _, file := range files {
log.Println(file.Filename)
}
c.String(http.StatusOK, fmt.Printf("%d files uploaded!", len(files)))
})
router.Run(":8080")
}
```
How to `curl`:
```bash
curl -X POST http://localhost:8080/upload \
-F "upload[]=@/Users/appleboy/test1.zip" \
-F "upload[]=@/Users/appleboy/test2.zip" \
-H "Content-Type: multipart/form-data"
```
### Grouping routes
```go
func main() {
router := gin.Default()
@ -271,14 +346,14 @@ func main() {
}
```
#### Blank Gin without middleware by default
### Blank Gin without middleware by default
Use
```go
r := gin.New()
```
instead of
```go
@ -286,7 +361,7 @@ r := gin.Default()
```
#### Using middleware
### Using middleware
```go
func main() {
// Creates a router without any middleware by default
@ -321,7 +396,7 @@ func main() {
}
```
#### Model binding and validation
### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
@ -371,13 +446,48 @@ func main() {
}
```
### Bind Query String
See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292).
```go
package main
import "log"
import "github.com/gin-gonic/gin"
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
func main() {
route := gin.Default()
route.GET("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/develop/binding/binding.go#L45
if c.Bind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
}
c.String(200, "Success")
}
```
### Multipart/Urlencoded binding
###Multipart/Urlencoded binding
```go
package main
import (
"gopkg.in/gin-gonic/gin.v1"
"github.com/gin-gonic/gin"
)
type LoginForm struct {
@ -389,7 +499,7 @@ func main() {
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
// you can bind multipart form with explicit binding declaration:
// c.BindWith(&form, binding.Form)
// c.MustBindWith(&form, binding.Form)
// or you can simply use autobinding with Bind method:
var form LoginForm
// in this case proper binding will be automatically selected
@ -410,8 +520,7 @@ Test it with:
$ curl -v --form user=user --form password=password http://localhost:8080/login
```
#### XML, JSON and YAML rendering
### XML, JSON and YAML rendering
```go
func main() {
@ -450,7 +559,7 @@ func main() {
}
```
####Serving static files
### Serving static files
```go
func main() {
@ -464,9 +573,9 @@ func main() {
}
```
####HTML rendering
### HTML rendering
Using LoadHTMLTemplates()
Using LoadHTMLGlob() or LoadHTMLFiles()
```go
func main() {
@ -481,7 +590,9 @@ func main() {
router.Run(":8080")
}
```
templates/index.tmpl
```html
<html>
<h1>
@ -509,7 +620,9 @@ func main() {
router.Run(":8080")
}
```
templates/posts/index.tmpl
```html
{{ define "posts/index.tmpl" }}
<html><h1>
@ -519,7 +632,9 @@ templates/posts/index.tmpl
</html>
{{ end }}
```
templates/users/index.tmpl
```html
{{ define "users/index.tmpl" }}
<html><h1>
@ -543,8 +658,59 @@ func main() {
}
```
You may use custom delims
#### Redirects
```go
r := gin.Default()
r.Delims("{[{", "}]}")
r.LoadHTMLGlob("/path/to/templates"))
```
#### Add custom template funcs
main.go
```go
...
func formatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d/%02d/%02d", year, month, day)
}
...
router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
...
router.GET("/raw", func(c *Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
...
```
raw.tmpl
```html
Date: {[{.now | formatAsDate}]}
```
Result:
```
Date: 2017/07/01
```
### Multitemplate
Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
### Redirects
Issuing a HTTP redirect is easy:
@ -556,7 +722,7 @@ r.GET("/test", func(c *gin.Context) {
Both internal and external locations are supported.
#### Custom Middleware
### Custom Middleware
```go
func Logger() gin.HandlerFunc {
@ -596,7 +762,8 @@ func main() {
}
```
#### Using BasicAuth() middleware
### Using BasicAuth() middleware
```go
// simulate some private data
var secrets = gin.H{
@ -634,8 +801,8 @@ func main() {
}
```
### Goroutines inside a middleware
#### Goroutines inside a middleware
When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy.
```go
@ -667,7 +834,7 @@ func main() {
}
```
#### Custom HTTP configuration
### Custom HTTP configuration
Use `http.ListenAndServe()` directly, like this:
@ -694,7 +861,66 @@ func main() {
}
```
#### Graceful restart or stop
### Support Let's Encrypt
example for 1-line LetsEncrypt HTTPS servers.
[embedmd]:# (examples/auto-tls/example1.go go)
```go
package main
import (
"log"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Ping handler
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
}
```
example for custom autocert manager.
[embedmd]:# (examples/auto-tls/example2.go go)
```go
package main
import (
"log"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/acme/autocert"
)
func main() {
r := gin.Default()
// Ping handler
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"),
Cache: autocert.DirCache("/var/www/.cache"),
}
log.Fatal(autotls.RunWithManager(r, &m))
}
```
### Graceful restart or stop
Do you want to graceful restart or stop your web server?
There are some ways this can be done.
@ -711,21 +937,64 @@ endless.ListenAndServe(":4242", router)
An alternative to endless:
* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully.
* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server.
* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers.
## Contributing
If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](./examples/graceful-shutdown) example with gin.
- 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.
- With pull requests:
- Open your pull request against develop
- 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.
[embedmd]:# (examples/graceful-shutdown/graceful-shutdown/server.go go)
```go
// +build go1.8
## Example
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil {
log.Printf("listen: %s\n", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("Server exist")
}
```
## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.

20
auth.go
View File

@ -10,16 +10,18 @@ import (
"strconv"
)
// AuthUserKey is the cookie name for user credential in basic auth
const AuthUserKey = "user"
type (
Accounts map[string]string
authPair struct {
Value string
User string
}
authPairs []authPair
)
// Accounts defines a key/value for user/pass list of authorized logins
type Accounts map[string]string
type authPair struct {
Value string
User string
}
type authPairs []authPair
func (a authPairs) searchCredential(authValue string) (string, bool) {
if len(authValue) == 0 {
@ -87,6 +89,6 @@ func secureCompare(given, actual string) bool {
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
}
/* Securely compare actual to itself to keep constant time, but always return false */
// Securely compare actual to itself to keep constant time, but always return false
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
}

View File

@ -1,3 +1,7 @@
// Copyright 2017 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 (

View File

@ -15,6 +15,8 @@ const (
MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack"
)
type Binding interface {
@ -40,22 +42,25 @@ var (
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
)
func Default(method, contentType string) Binding {
if method == "GET" {
return Form
} else {
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
return Form
}
}
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
return Form
}
}

View File

@ -12,17 +12,17 @@ import (
"github.com/gin-gonic/gin/binding/example"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
type FooStruct struct {
Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"`
Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
}
type FooBarStruct struct {
FooStruct
Bar string `json:"bar" form:"bar" xml:"bar" binding:"required"`
Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"`
}
func TestBindingDefault(t *testing.T) {
@ -43,6 +43,9 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, Default("POST", MIMEPROTOBUF), ProtoBuf)
assert.Equal(t, Default("PUT", MIMEPROTOBUF), ProtoBuf)
assert.Equal(t, Default("POST", MIMEMSGPACK), MsgPack)
assert.Equal(t, Default("PUT", MIMEMSGPACK2), MsgPack)
}
func TestBindingJSON(t *testing.T) {
@ -121,6 +124,26 @@ func TestBindingProtoBuf(t *testing.T) {
string(data), string(data[1:]))
}
func TestBindingMsgPack(t *testing.T) {
test := FooStruct{
Foo: "bar",
}
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test)
assert.NoError(t, err)
data := buf.Bytes()
testMsgPackBodyBinding(t,
MsgPack, "msgpack",
"/", "/",
string(data), string(data[1:]))
}
func TestValidationFails(t *testing.T) {
var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`)
@ -213,6 +236,23 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba
assert.Error(t, err)
}
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, b.Name(), name)
obj := FooStruct{}
req := requestWithBody("POST", path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
assert.NoError(t, err)
assert.Equal(t, obj.Foo, "bar")
obj = FooStruct{}
req = requestWithBody("POST", badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
assert.Error(t, err)
}
func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return

View File

@ -1,3 +1,7 @@
// Copyright 2017 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 binding
import (

View File

@ -8,6 +8,7 @@ import (
"errors"
"reflect"
"strconv"
"time"
)
func mapForm(ptr interface{}, form map[string][]string) error {
@ -52,6 +53,12 @@ func mapForm(ptr interface{}, form map[string][]string) error {
}
val.Field(i).Set(slice)
} else {
if _, isTime := structField.Interface().(time.Time); isTime {
if err := setTimeField(inputValue[0], typeField, structField); err != nil {
return err
}
continue
}
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err
}
@ -140,6 +147,31 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
return err
}
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")
}
if val == "" {
value.Set(reflect.ValueOf(time.Time{}))
return nil
}
l := time.Local
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
l = time.UTC
}
t, err := time.ParseInLocation(timeFormat, val, l)
if err != nil {
return err
}
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

View File

@ -6,7 +6,6 @@ package binding
import (
"encoding/json"
"net/http"
)

28
binding/msgpack.go Normal file
View File

@ -0,0 +1,28 @@
// Copyright 2017 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 binding
import (
"net/http"
"github.com/ugorji/go/codec"
)
type msgpackBinding struct{}
func (msgpackBinding) Name() string {
return "msgpack"
}
func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
if err := codec.NewDecoder(req.Body, new(codec.MsgpackHandle)).Decode(&obj); err != nil {
//var decoder *codec.Decoder = codec.NewDecoder(req.Body, &codec.MsgpackHandle)
//if err := decoder.Decode(&obj); err != nil {
return err
}
return validate(obj)
}

View File

@ -5,10 +5,10 @@
package binding
import (
"github.com/golang/protobuf/proto"
"io/ioutil"
"net/http"
"github.com/golang/protobuf/proto"
)
type protobufBinding struct{}

View File

@ -7,17 +7,18 @@ package gin
import (
"errors"
"io"
"io/ioutil"
"math"
"mime/multipart"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/manucorporat/sse"
"golang.org/x/net/context"
)
// Content-Type MIME of the most common data formats
@ -31,7 +32,10 @@ const (
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
)
const abortIndex int8 = math.MaxInt8 / 2
const (
defaultMemory = 32 << 20 // 32 MB
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.
@ -50,8 +54,6 @@ type Context struct {
Accepted []string
}
var _ context.Context = &Context{}
/************************************/
/********** CONTEXT CREATION ********/
/************************************/
@ -67,7 +69,7 @@ func (c *Context) reset() {
}
// Copy returns a copy of the current context that can be safely used outside the request's scope.
// This have to be used then the context has to be passed to a goroutine.
// This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context {
var cp = *c
cp.writermem.ResponseWriter = nil
@ -83,13 +85,18 @@ func (c *Context) HandlerName() string {
return nameOfFunction(c.handlers.Last())
}
// Handler returns the main handler.
func (c *Context) Handler() HandlerFunc {
return c.handlers.Last()
}
/************************************/
/*********** FLOW CONTROL ***********/
/************************************/
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in github.
// See example in GitHub.
func (c *Context) Next() {
c.index++
s := int8(len(c.handlers))
@ -112,13 +119,20 @@ func (c *Context) Abort() {
}
// AbortWithStatus calls `Abort()` and writes the headers with the specified status code.
// For example, a failed attempt to authentificate a request could use: context.AbortWithStatus(401).
// For example, a failed attempt to authenticate a request could use: context.AbortWithStatus(401).
func (c *Context) AbortWithStatus(code int) {
c.Status(code)
c.Writer.WriteHeaderNow()
c.Abort()
}
// AbortWithStatusJSON calls `Abort()` and then `JSON` internally. This method stops the chain, writes the status code and return a JSON body
// It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) {
c.Abort()
c.JSON(code, jsonObj)
}
// AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and
// pushes the specified error to `c.Errors`.
// See Context.Error() for more details.
@ -135,7 +149,11 @@ func (c *Context) AbortWithError(code int, err error) *Error {
// It's a good idea to call Error for each error that occurred during the resolution of a request.
// A middleware can be used to collect all the errors
// and push them to a database together, print a log, or append it in the HTTP response.
// Error will panic if err is nil.
func (c *Context) Error(err error) *Error {
if err == nil {
panic("err is nil")
}
var parsedError *Error
switch err.(type) {
case *Error:
@ -154,7 +172,7 @@ func (c *Context) Error(err error) *Error {
/******** METADATA MANAGEMENT********/
/************************************/
// Set is used to store a new key/value pair exclusivelly for this context.
// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) {
if c.Keys == nil {
@ -166,9 +184,7 @@ func (c *Context) Set(key string, value interface{}) {
// Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) {
if c.Keys != nil {
value, exists = c.Keys[key]
}
value, exists = c.Keys[key]
return
}
@ -180,6 +196,94 @@ func (c *Context) MustGet(key string) interface{} {
panic("Key \"" + key + "\" does not exist")
}
// GetString returns the value associated with the key as a string.
func (c *Context) GetString(key string) (s string) {
if val, ok := c.Get(key); ok && val != nil {
s, _ = val.(string)
}
return
}
// GetBool returns the value associated with the key as a boolean.
func (c *Context) GetBool(key string) (b bool) {
if val, ok := c.Get(key); ok && val != nil {
b, _ = val.(bool)
}
return
}
// GetInt returns the value associated with the key as an integer.
func (c *Context) GetInt(key string) (i int) {
if val, ok := c.Get(key); ok && val != nil {
i, _ = val.(int)
}
return
}
// GetInt64 returns the value associated with the key as an integer.
func (c *Context) GetInt64(key string) (i64 int64) {
if val, ok := c.Get(key); ok && val != nil {
i64, _ = val.(int64)
}
return
}
// GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) {
if val, ok := c.Get(key); ok && val != nil {
f64, _ = val.(float64)
}
return
}
// GetTime returns the value associated with the key as time.
func (c *Context) GetTime(key string) (t time.Time) {
if val, ok := c.Get(key); ok && val != nil {
t, _ = val.(time.Time)
}
return
}
// GetDuration returns the value associated with the key as a duration.
func (c *Context) GetDuration(key string) (d time.Duration) {
if val, ok := c.Get(key); ok && val != nil {
d, _ = val.(time.Duration)
}
return
}
// GetStringSlice returns the value associated with the key as a slice of strings.
func (c *Context) GetStringSlice(key string) (ss []string) {
if val, ok := c.Get(key); ok && val != nil {
ss, _ = val.([]string)
}
return
}
// GetStringMap returns the value associated with the key as a map of interfaces.
func (c *Context) GetStringMap(key string) (sm map[string]interface{}) {
if val, ok := c.Get(key); ok && val != nil {
sm, _ = val.(map[string]interface{})
}
return
}
// GetStringMapString returns the value associated with the key as a map of strings.
func (c *Context) GetStringMapString(key string) (sms map[string]string) {
if val, ok := c.Get(key); ok && val != nil {
sms, _ = val.(map[string]string)
}
return
}
// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {
if val, ok := c.Get(key); ok && val != nil {
smss, _ = val.(map[string][]string)
}
return
}
/************************************/
/************ INPUT DATA ************/
/************************************/
@ -195,7 +299,7 @@ func (c *Context) Param(key string) string {
}
// Query returns the keyed url query value if it exists,
// othewise it returns an empty string `("")`.
// otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
// GET /path?id=1234&name=Manu&value=
// c.Query("id") == "1234"
@ -208,7 +312,7 @@ func (c *Context) Query(key string) string {
}
// DefaultQuery returns the keyed url query value if it exists,
// othewise it returns the specified defaultValue string.
// otherwise it returns the specified defaultValue string.
// See: Query() and GetQuery() for further information.
// GET /?name=Manu&lastname=
// c.DefaultQuery("name", "unknown") == "Manu"
@ -223,7 +327,7 @@ func (c *Context) DefaultQuery(key, defaultValue string) string {
// GetQuery is like Query(), it returns the keyed url query value
// if it exists `(value, true)` (even when the value is an empty string),
// othewise it returns `("", false)`.
// otherwise it returns `("", false)`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
// GET /?name=Manu&lastname=
// ("Manu", true) == c.GetQuery("name")
@ -296,7 +400,7 @@ func (c *Context) PostFormArray(key string) []string {
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request
req.ParseForm()
req.ParseMultipartForm(32 << 20) // 32 MB
req.ParseMultipartForm(defaultMemory)
if values := req.PostForm[key]; len(values) > 0 {
return values, true
}
@ -308,6 +412,18 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false
}
// 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)
return fh, err
}
// MultipartForm is the parsed multipart form, including file uploads.
func (c *Context) MultipartForm() (*multipart.Form, error) {
err := c.Request.ParseMultipartForm(defaultMemory)
return c.Request.MultipartForm, err
}
// Bind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding
@ -318,33 +434,38 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
// Like ParseBody() but this method also writes a 400 error if the json is not valid.
func (c *Context) Bind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.BindWith(obj, b)
return c.MustBindWith(obj, b)
}
// BindJSON is a shortcut for c.BindWith(obj, binding.JSON)
// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON)
func (c *Context) BindJSON(obj interface{}) error {
return c.BindWith(obj, binding.JSON)
return c.MustBindWith(obj, binding.JSON)
}
// BindWith binds the passed struct pointer using the specified binding engine.
// MustBindWith binds the passed struct pointer using the specified binding
// engine. It will abort the request with HTTP 400 if any error ocurrs.
// See the binding package.
func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
if err := b.Bind(c.Request, obj); err != nil {
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)
return err
}
return nil
return
}
// ShouldBindWith binds the passed struct pointer using the specified binding
// engine.
// See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
// 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 := strings.TrimSpace(c.requestHeader("X-Real-Ip"))
if len(clientIP) > 0 {
return clientIP
}
clientIP = c.requestHeader("X-Forwarded-For")
clientIP := c.requestHeader("X-Forwarded-For")
if index := strings.IndexByte(clientIP, ','); index >= 0 {
clientIP = clientIP[0:index]
}
@ -352,10 +473,22 @@ func (c *Context) ClientIP() string {
if len(clientIP) > 0 {
return clientIP
}
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
if len(clientIP) > 0 {
return clientIP
}
}
if c.engine.AppEngine {
if addr := c.Request.Header.Get("X-Appengine-Remote-Addr"); addr != "" {
return addr
}
}
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
return ip
}
return ""
}
@ -364,6 +497,16 @@ func (c *Context) ContentType() string {
return filterFlags(c.requestHeader("Content-Type"))
}
// IsWebsocket returns true if the request headers indicate that a websocket
// handshake is being initiated by the client.
func (c *Context) IsWebsocket() bool {
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
return true
}
return false
}
func (c *Context) requestHeader(key string) string {
if values, _ := c.Request.Header[key]; len(values) > 0 {
return values[0]
@ -375,6 +518,19 @@ func (c *Context) requestHeader(key string) string {
/******** RESPONSE RENDERING ********/
/************************************/
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function
func bodyAllowedForStatus(status int) bool {
switch {
case status >= 100 && status <= 199:
return false
case status == 204:
return false
case status == 304:
return false
}
return true
}
func (c *Context) Status(code int) {
c.writermem.WriteHeader(code)
}
@ -390,6 +546,16 @@ func (c *Context) Header(key, value string) {
}
}
// GetHeader returns value from request headers
func (c *Context) GetHeader(key string) string {
return c.requestHeader(key)
}
// GetRawData return stream data
func (c *Context) GetRawData() ([]byte, error) {
return ioutil.ReadAll(c.Request.Body)
}
func (c *Context) SetCookie(
name string,
value string,
@ -424,6 +590,13 @@ func (c *Context) Cookie(name string) (string, error) {
func (c *Context) Render(code int, r render.Render) {
c.Status(code)
if !bodyAllowedForStatus(code) {
r.WriteContentType(c.Writer)
c.Writer.WriteHeaderNow()
return
}
if err := r.Render(c.Writer); err != nil {
panic(err)
}
@ -439,7 +612,7 @@ func (c *Context) HTML(code int, name string, obj interface{}) {
// IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body.
// It also sets the Content-Type as "application/json".
// WARNING: we recommend to use this only for development propuses since printing pretty JSON is
// WARNING: we recommend to use this only for development purposes since printing pretty JSON is
// more CPU and bandwidth consuming. Use Context.JSON() instead.
func (c *Context) IndentedJSON(code int, obj interface{}) {
c.Render(code, render.IndentedJSON{Data: obj})
@ -448,10 +621,7 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
// 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.Status(code)
if err := render.WriteJSON(c.Writer, obj); err != nil {
panic(err)
}
c.Render(code, render.JSON{Data: obj})
}
// XML serializes the given struct as XML into the response body.
@ -467,8 +637,7 @@ func (c *Context) YAML(code int, obj interface{}) {
// String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) {
c.Status(code)
render.WriteString(c.Writer, format, values)
c.Render(code, render.String{Format: format, Data: values})
}
// Redirect returns a HTTP redirect to the specific location.

11
context_appengine.go Normal file
View File

@ -0,0 +1,11 @@
// +build appengine
// Copyright 2017 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
func init() {
defaultAppEngine = true
}

View File

@ -7,18 +7,23 @@ package gin
import (
"bytes"
"errors"
"fmt"
"html/template"
"mime/multipart"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
"github.com/manucorporat/sse"
"github.com/gin-contrib/sse"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
var _ context.Context = &Context{}
// Unit tests TODO
// func (c *Context) File(filepath string) {
// func (c *Context) Negotiate(code int, config Negotiate) {
@ -38,6 +43,8 @@ func createMultipartRequest() *http.Request {
must(mw.WriteField("array", "first"))
must(mw.WriteField("array", "second"))
must(mw.WriteField("id", ""))
must(mw.WriteField("time_local", "31/12/2016 14:55"))
must(mw.WriteField("time_utc", "31/12/2016 14:55"))
req, err := http.NewRequest("POST", "/", body)
must(err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
@ -50,6 +57,37 @@ func must(err error) {
}
}
func TestContextFormFile(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) {
w.Write([]byte("test"))
}
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", buf)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
f, err := c.FormFile("file")
if assert.NoError(t, err) {
assert.Equal(t, "test", f.Filename)
}
}
func TestContextMultipartForm(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.WriteField("foo", "bar")
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", buf)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
f, err := c.MultipartForm()
if assert.NoError(t, err) {
assert.NotNil(t, f)
}
}
func TestContextReset(t *testing.T) {
router := New()
c := router.allocateContext()
@ -74,7 +112,7 @@ func TestContextReset(t *testing.T) {
}
func TestContextHandlers(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
assert.Nil(t, c.handlers)
assert.Nil(t, c.handlers.Last())
@ -95,7 +133,7 @@ func TestContextHandlers(t *testing.T) {
// TestContextSetGet tests that a parameter is set correctly on the
// current context and can be retrieved using Get.
func TestContextSetGet(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("foo", "bar")
value, err := c.Get("foo")
@ -111,7 +149,7 @@ func TestContextSetGet(t *testing.T) {
}
func TestContextSetGetValues(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("string", "this is a string")
c.Set("int32", int32(-42))
c.Set("int64", int64(42424242424242))
@ -131,8 +169,87 @@ func TestContextSetGetValues(t *testing.T) {
}
func TestContextGetString(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("string", "this is a string")
assert.Equal(t, "this is a string", c.GetString("string"))
}
func TestContextSetGetBool(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("bool", true)
assert.Equal(t, true, c.GetBool("bool"))
}
func TestContextGetInt(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("int", 1)
assert.Equal(t, 1, c.GetInt("int"))
}
func TestContextGetInt64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("int64", int64(42424242424242))
assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
}
func TestContextGetFloat64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("float64", 4.2)
assert.Equal(t, 4.2, c.GetFloat64("float64"))
}
func TestContextGetTime(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
t1, _ := time.Parse("1/2/2006 15:04:05", "01/01/2017 12:00:00")
c.Set("time", t1)
assert.Equal(t, t1, c.GetTime("time"))
}
func TestContextGetDuration(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("duration", time.Second)
assert.Equal(t, time.Second, c.GetDuration("duration"))
}
func TestContextGetStringSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("slice", []string{"foo"})
assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice"))
}
func TestContextGetStringMap(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
var m = make(map[string]interface{})
m["foo"] = 1
c.Set("map", m)
assert.Equal(t, m, c.GetStringMap("map"))
assert.Equal(t, 1, c.GetStringMap("map")["foo"])
}
func TestContextGetStringMapString(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
var m = make(map[string]string)
m["foo"] = "bar"
c.Set("map", m)
assert.Equal(t, m, c.GetStringMapString("map"))
assert.Equal(t, "bar", c.GetStringMapString("map")["foo"])
}
func TestContextGetStringMapStringSlice(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
var m = make(map[string][]string)
m["foo"] = []string{"foo"}
c.Set("map", m)
assert.Equal(t, m, c.GetStringMapStringSlice("map"))
assert.Equal(t, []string{"foo"}, c.GetStringMapStringSlice("map")["foo"])
}
func TestContextCopy(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.index = 2
c.Request, _ = http.NewRequest("POST", "/hola", nil)
c.handlers = HandlersChain{func(c *Context) {}}
@ -151,7 +268,7 @@ func TestContextCopy(t *testing.T) {
}
func TestContextHandlerName(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName())
@ -161,8 +278,19 @@ func handlerNameTest(c *Context) {
}
var handlerTest HandlerFunc = func(c *Context) {
}
func TestContextHandler(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.handlers = HandlersChain{func(c *Context) {}, handlerTest}
assert.Equal(t, reflect.ValueOf(handlerTest).Pointer(), reflect.ValueOf(c.Handler()).Pointer())
}
func TestContextQuery(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil)
value, ok := c.GetQuery("foo")
@ -197,7 +325,7 @@ func TestContextQuery(t *testing.T) {
}
func TestContextQueryAndPostForm(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body)
c.Request.Header.Add("Content-Type", MIMEPOSTForm)
@ -270,15 +398,18 @@ func TestContextQueryAndPostForm(t *testing.T) {
}
func TestContextPostFormMultipart(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request = createMultipartRequest()
var obj struct {
Foo string `form:"foo"`
Bar string `form:"bar"`
BarAsInt int `form:"bar"`
Array []string `form:"array"`
ID string `form:"id"`
Foo string `form:"foo"`
Bar string `form:"bar"`
BarAsInt int `form:"bar"`
Array []string `form:"array"`
ID string `form:"id"`
TimeLocal time.Time `form:"time_local" time_format:"02/01/2006 15:04"`
TimeUTC time.Time `form:"time_utc" time_format:"02/01/2006 15:04" time_utc:"1"`
BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"`
}
assert.NoError(t, c.Bind(&obj))
assert.Equal(t, obj.Foo, "bar")
@ -286,6 +417,11 @@ func TestContextPostFormMultipart(t *testing.T) {
assert.Equal(t, obj.BarAsInt, 10)
assert.Equal(t, obj.Array, []string{"first", "second"})
assert.Equal(t, obj.ID, "")
assert.Equal(t, obj.TimeLocal.Format("02/01/2006 15:04"), "31/12/2016 14:55")
assert.Equal(t, obj.TimeLocal.Location(), time.Local)
assert.Equal(t, obj.TimeUTC.Format("02/01/2006 15:04"), "31/12/2016 14:55")
assert.Equal(t, obj.TimeUTC.Location(), time.UTC)
assert.True(t, obj.BlankTime.IsZero())
value, ok := c.GetQuery("foo")
assert.False(t, ok)
@ -334,46 +470,115 @@ func TestContextPostFormMultipart(t *testing.T) {
}
func TestContextSetCookie(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
}
func TestContextSetCookiePathEmpty(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "", "localhost", true, true)
assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
}
func TestContextGetCookie(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/get", nil)
c.Request.Header.Set("Cookie", "user=gin")
cookie, _ := c.Cookie("user")
assert.Equal(t, cookie, "gin")
_, err := c.Cookie("nokey")
assert.Error(t, err)
}
func TestContextBodyAllowedForStatus(t *testing.T) {
assert.Equal(t, false, bodyAllowedForStatus(102))
assert.Equal(t, false, bodyAllowedForStatus(204))
assert.Equal(t, false, bodyAllowedForStatus(304))
assert.Equal(t, true, bodyAllowedForStatus(500))
}
type TestPanicRender struct {
}
func (*TestPanicRender) Render(http.ResponseWriter) error {
return errors.New("TestPanicRender")
}
func (*TestPanicRender) WriteContentType(http.ResponseWriter) {}
func TestContextRenderPanicIfErr(t *testing.T) {
defer func() {
r := recover()
assert.Equal(t, "TestPanicRender", fmt.Sprint(r))
}()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Render(http.StatusOK, &TestPanicRender{})
assert.Fail(t, "Panic not detected")
}
// Tests that the response is serialized as JSON
// and Content-Type is set to application/json
func TestContextRenderJSON(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.JSON(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201)
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
assert.Equal(t, 201, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
// Tests that no JSON is rendered if code is 204
func TestContextRenderNoContentJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.JSON(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
// Tests that the response is serialized as JSON
// we change the content-type before
func TestContextRenderAPIJSON(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "application/vnd.api+json")
c.JSON(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201)
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
assert.Equal(t, 201, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type"))
}
// Tests that no Custom JSON is rendered if code is 204
func TestContextRenderNoContentAPIJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "application/vnd.api+json")
c.JSON(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json")
}
// Tests that the response is serialized as JSON
// and Content-Type is set to application/json
func TestContextRenderIndentedJSON(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
assert.Equal(t, w.Code, 201)
@ -381,10 +586,23 @@ func TestContextRenderIndentedJSON(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
}
// Tests that no Custom JSON is rendered if code is 204
func TestContextRenderNoContentIndentedJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
}
// Tests that the response executes the templates
// and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) {
c, w, router := CreateTestContext()
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
@ -395,10 +613,26 @@ func TestContextRenderHTML(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
// Tests that no HTML is rendered if code is 204
func TestContextRenderNoContentHTML(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
c.HTML(204, "t", H{"name": "alexandernyquist"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
// TestContextXML tests that the response is serialized as XML
// and Content-Type is set to application/xml
func TestContextRenderXML(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.XML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201)
@ -406,10 +640,24 @@ func TestContextRenderXML(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
}
// Tests that no XML is rendered if code is 204
func TestContextRenderNoContentXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.XML(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
}
// TestContextString tests that the response is returned
// with Content-Type set to text/plain
func TestContextRenderString(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.String(201, "test %s %d", "string", 2)
assert.Equal(t, w.Code, 201)
@ -417,10 +665,24 @@ func TestContextRenderString(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
}
// Tests that no String is rendered if code is 204
func TestContextRenderNoContentString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.String(204, "test %s %d", "string", 2)
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
}
// TestContextString tests that the response is returned
// with Content-Type set to text/html
func TestContextRenderHTMLString(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(201, "<html>%s %d</html>", "string", 3)
@ -429,10 +691,25 @@ func TestContextRenderHTMLString(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
// Tests that no HTML String is rendered if code is 204
func TestContextRenderNoContentHTMLString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(204, "<html>%s %d</html>", "string", 3)
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
// TestContextData tests that the response can be written from `bytesting`
// with specified MIME type
func TestContextRenderData(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Data(201, "text/csv", []byte(`foo,bar`))
assert.Equal(t, w.Code, 201)
@ -440,8 +717,22 @@ func TestContextRenderData(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
}
// Tests that no Custom Data is rendered if code is 204
func TestContextRenderNoContentData(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Data(204, "text/csv", []byte(`foo,bar`))
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
}
func TestContextRenderSSE(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.SSEvent("float", 1.5)
c.Render(-1, sse.Event{
Id: "123",
@ -456,7 +747,9 @@ func TestContextRenderSSE(t *testing.T) {
}
func TestContextRenderFile(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.File("./gin.go")
@ -468,7 +761,9 @@ func TestContextRenderFile(t *testing.T) {
// TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/x-yaml
func TestContextRenderYAML(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.YAML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201)
@ -477,7 +772,7 @@ func TestContextRenderYAML(t *testing.T) {
}
func TestContextHeaders(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Header("Content-Type", "text/plain")
c.Header("X-Custom", "value")
@ -494,7 +789,9 @@ func TestContextHeaders(t *testing.T) {
// TODO
func TestContextRenderRedirectWithRelativePath(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
assert.Panics(t, func() { c.Redirect(299, "/new_path") })
assert.Panics(t, func() { c.Redirect(309, "/new_path") })
@ -506,7 +803,9 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) {
}
func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Redirect(302, "http://google.com")
c.Writer.WriteHeaderNow()
@ -516,7 +815,9 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
}
func TestContextRenderRedirectWith201(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Redirect(201, "/resource")
c.Writer.WriteHeaderNow()
@ -526,7 +827,7 @@ func TestContextRenderRedirectWith201(t *testing.T) {
}
func TestContextRenderRedirectAll(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
assert.Panics(t, func() { c.Redirect(200, "/resource") })
assert.Panics(t, func() { c.Redirect(202, "/resource") })
@ -536,8 +837,70 @@ func TestContextRenderRedirectAll(t *testing.T) {
assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
}
func TestContextNegotiationWithJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(200, Negotiate{
Offered: []string{MIMEJSON, MIMEXML},
Data: H{"foo": "bar"},
})
assert.Equal(t, 200, w.Code)
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
func TestContextNegotiationWithXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(200, Negotiate{
Offered: []string{MIMEXML, MIMEJSON},
Data: H{"foo": "bar"},
})
assert.Equal(t, 200, w.Code)
assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
func TestContextNegotiationWithHTML(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
c.Negotiate(200, Negotiate{
Offered: []string{MIMEHTML},
Data: H{"name": "gin"},
HTMLName: "t",
})
assert.Equal(t, 200, w.Code)
assert.Equal(t, "Hello gin", w.Body.String())
assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
func TestContextNegotiationNotSupport(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
c.Negotiate(200, Negotiate{
Offered: []string{MIMEPOSTForm},
})
assert.Equal(t, 406, w.Code)
assert.Equal(t, c.index, abortIndex)
assert.True(t, c.IsAborted())
}
func TestContextNegotiationFormat(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "", nil)
assert.Panics(t, func() { c.NegotiateFormat() })
@ -546,7 +909,7 @@ func TestContextNegotiationFormat(t *testing.T) {
}
func TestContextNegotiationFormatWithAccept(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
@ -556,7 +919,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) {
}
func TestContextNegotiationFormatCustum(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
@ -569,7 +932,7 @@ func TestContextNegotiationFormatCustum(t *testing.T) {
}
func TestContextIsAborted(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
assert.False(t, c.IsAborted())
c.Abort()
@ -585,7 +948,9 @@ func TestContextIsAborted(t *testing.T) {
// TestContextData tests that the response can be written from `bytesting`
// with specified MIME type
func TestContextAbortWithStatus(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.index = 4
c.AbortWithStatus(401)
@ -595,8 +960,38 @@ func TestContextAbortWithStatus(t *testing.T) {
assert.True(t, c.IsAborted())
}
type testJSONAbortMsg struct {
Foo string `json:"foo"`
Bar string `json:"bar"`
}
func TestContextAbortWithStatusJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.index = 4
in := new(testJSONAbortMsg)
in.Bar = "barValue"
in.Foo = "fooValue"
c.AbortWithStatusJSON(415, in)
assert.Equal(t, c.index, abortIndex)
assert.Equal(t, c.Writer.Status(), 415)
assert.Equal(t, w.Code, 415)
assert.True(t, c.IsAborted())
contentType := w.Header().Get("Content-Type")
assert.Equal(t, contentType, "application/json; charset=utf-8")
buf := new(bytes.Buffer)
buf.ReadFrom(w.Body)
jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
}
func TestContextError(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
assert.Empty(t, c.Errors)
c.Error(errors.New("first error"))
@ -619,10 +1014,17 @@ func TestContextError(t *testing.T) {
assert.Equal(t, c.Errors[1].Type, ErrorTypePublic)
assert.Equal(t, c.Errors.Last(), c.Errors[1])
defer func() {
if recover() == nil {
t.Error("didn't panic")
}
}()
c.Error(nil)
}
func TestContextTypedError(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
@ -636,7 +1038,9 @@ func TestContextTypedError(t *testing.T) {
}
func TestContextAbortWithError(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
assert.Equal(t, w.Code, 401)
@ -645,27 +1049,37 @@ func TestContextAbortWithError(t *testing.T) {
}
func TestContextClientIP(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
assert.Equal(t, c.ClientIP(), "10.10.10.10")
c.Request.Header.Del("X-Real-IP")
assert.Equal(t, c.ClientIP(), "20.20.20.20")
c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ")
assert.Equal(t, c.ClientIP(), "30.30.30.30")
assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For")
assert.Equal(t, c.ClientIP(), "40.40.40.40")
assert.Equal(t, "10.10.10.10", c.ClientIP())
c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ")
assert.Equal(t, "30.30.30.30", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For")
c.Request.Header.Del("X-Real-IP")
c.engine.AppEngine = true
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.Request.Header.Del("X-Appengine-Remote-Addr")
assert.Equal(t, "40.40.40.40", c.ClientIP())
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Equal(t, "", c.ClientIP())
}
func TestContextContentType(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
@ -673,7 +1087,7 @@ func TestContextContentType(t *testing.T) {
}
func TestContextAutoBindJSON(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON)
@ -688,7 +1102,9 @@ func TestContextAutoBindJSON(t *testing.T) {
}
func TestContextBindWithJSON(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
@ -703,7 +1119,9 @@ func TestContextBindWithJSON(t *testing.T) {
}
func TestContextBadAutoBind(t *testing.T) {
c, w, _ := CreateTestContext()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct {
@ -722,7 +1140,7 @@ func TestContextBadAutoBind(t *testing.T) {
}
func TestContextGolangContext(t *testing.T) {
c, _, _ := CreateTestContext()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
assert.NoError(t, c.Err())
assert.Nil(t, c.Done())
@ -736,3 +1154,45 @@ func TestContextGolangContext(t *testing.T) {
assert.Equal(t, c.Value("foo"), "bar")
assert.Nil(t, c.Value(1))
}
func TestWebsocketsRequired(t *testing.T) {
// Example request from spec: https://tools.ietf.org/html/rfc6455#section-1.2
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/chat", nil)
c.Request.Header.Set("Host", "server.example.com")
c.Request.Header.Set("Upgrade", "websocket")
c.Request.Header.Set("Connection", "Upgrade")
c.Request.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
c.Request.Header.Set("Origin", "http://example.com")
c.Request.Header.Set("Sec-WebSocket-Protocol", "chat, superchat")
c.Request.Header.Set("Sec-WebSocket-Version", "13")
assert.True(t, c.IsWebsocket())
// Normal request, no websocket required.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/chat", nil)
c.Request.Header.Set("Host", "server.example.com")
assert.False(t, c.IsWebsocket())
}
func TestGetRequestHeaderValue(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/chat", nil)
c.Request.Header.Set("Gin-Version", "1.0.0")
assert.Equal(t, "1.0.0", c.GetHeader("Gin-Version"))
assert.Equal(t, "", c.GetHeader("Connection"))
}
func TestContextGetRawData(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("Fetch binary post data")
c.Request, _ = http.NewRequest("POST", "/", body)
c.Request.Header.Add("Content-Type", MIMEPOSTForm)
data, err := c.GetRawData()
assert.Nil(t, err)
assert.Equal(t, "Fetch binary post data", string(data))
}

View File

@ -7,6 +7,7 @@ package gin
import (
"bytes"
"errors"
"html/template"
"io"
"log"
"os"
@ -66,6 +67,25 @@ func TestDebugPrintRoutes(t *testing.T) {
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, w.String())
}
func TestDebugPrintLoadTemplate(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl"))
debugPrintLoadTemplate(templ)
assert.Regexp(t, `^\[GIN-debug\] Loaded HTML Templates \(2\): \n(\t- \n|\t- hello\.tmpl\n){2}\n`, w.String())
}
func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
var w bytes.Buffer
setup(&w)
defer teardown()
debugPrintWARNINGSetHTMLTemplate()
assert.Equal(t, w.String(), "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n")
}
func setup(w io.Writer) {
SetMode(DebugMode)
log.SetOutput(w)

View File

@ -4,9 +4,18 @@
package gin
import "log"
import (
"log"
func (c *Context) GetCookie(name string) (string, error) {
log.Println("GetCookie() method is deprecated. Use Cookie() instead.")
return c.Cookie(name)
"github.com/gin-gonic/gin/binding"
)
// BindWith binds the passed struct pointer using the specified binding engine.
// See the binding package.
func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to
be deprecated, please check issue #662 and either use MustBindWith() if you
want HTTP 400 to be automatically returned if any error occur, of use
ShouldBindWith() if you need to manage the error.`)
return c.MustBindWith(obj, b)
}

6
doc.go Normal file
View File

@ -0,0 +1,6 @@
/*
Package gin implements a HTTP web framework called gin.
See https://gin-gonic.github.io/gin/ for more information about gin.
*/
package gin // import "github.com/gin-gonic/gin"

View File

@ -23,15 +23,13 @@ const (
ErrorTypeNu = 2
)
type (
Error struct {
Err error
Type ErrorType
Meta interface{}
}
type Error struct {
Err error
Type ErrorType
Meta interface{}
}
errorMsgs []*Error
)
type errorMsgs []*Error
var _ error = &Error{}
@ -72,7 +70,7 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
}
// Implements the error interface
func (msg *Error) Error() string {
func (msg Error) Error() string {
return msg.Err.Error()
}
@ -80,7 +78,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0
}
// Returns a readonly copy filterd the byte.
// Returns a readonly copy filtered the byte.
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic
func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
if len(a) == 0 {

View File

@ -54,6 +54,13 @@ func TestError(t *testing.T) {
"status": "200",
"data": "some data",
})
type customError struct {
status string
data string
}
err.SetMeta(customError{status: "200", data: "other data"})
assert.Equal(t, err.JSON(), customError{status: "200", data: "other data"})
}
func TestErrorSlice(t *testing.T) {

View File

@ -1,8 +1,9 @@
package hello
import (
"github.com/gin-gonic/gin"
"net/http"
"github.com/gin-gonic/gin"
)
// This function's name is a must. App Engine uses it to drive the requests properly.

View File

@ -0,0 +1,19 @@
package main
import (
"log"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Ping handler
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
}

View File

@ -0,0 +1,26 @@
package main
import (
"log"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/acme/autocert"
)
func main() {
r := gin.Default()
// Ping handler
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"),
Cache: autocert.DirCache("/var/www/.cache"),
}
log.Fatal(autotls.RunWithManager(r, &m))
}

View File

@ -7,6 +7,8 @@ import (
var DB = make(map[string]string)
func main() {
// Disable Console Color
// gin.DisableConsoleColor()
r := gin.Default()
// Ping test

View File

@ -0,0 +1,45 @@
// +build go1.8
package main
import (
"log"
"net/http"
"os"
"os/signal"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "Welcome Gin Server")
})
server := &http.Server{
Addr: ":8080",
Handler: router,
}
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
go func() {
<-quit
log.Println("receive interrupt signal")
if err := server.Close(); err != nil {
log.Fatal("Server Close:", err)
}
}()
if err := server.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
log.Println("Server closed under request")
} else {
log.Fatal("Server closed unexpect")
}
}
log.Println("Server exist")
}

View File

@ -0,0 +1,48 @@
// +build go1.8
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
time.Sleep(5 * time.Second)
c.String(http.StatusOK, "Welcome Gin Server")
})
srv := &http.Server{
Addr: ":8080",
Handler: router,
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil {
log.Printf("listen: %s\n", err)
}
}()
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 5 seconds.
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown:", err)
}
log.Println("Server exist")
}

View File

@ -0,0 +1,10 @@
all: deps build
.PHONY: deps
deps:
go get -d -v github.com/dustin/go-broadcast/...
go get -d -v github.com/manucorporat/stats/...
.PHONY: build
build: deps
go build -o realtime-advanced main.go rooms.go routes.go stats.go

View File

@ -11,7 +11,6 @@ import (
)
func rateLimit(c *gin.Context) {
ip := c.ClientIP()
value := int(ips.Add(ip, 1))
if value%50 == 0 {

View File

@ -8,11 +8,13 @@ import (
"github.com/manucorporat/stats"
)
var ips = stats.New()
var messages = stats.New()
var users = stats.New()
var mutexStats sync.RWMutex
var savedStats map[string]uint64
var (
ips = stats.New()
messages = stats.New()
users = stats.New()
mutexStats sync.RWMutex
savedStats map[string]uint64
)
func statsWorker() {
c := time.Tick(1 * time.Second)

View File

@ -0,0 +1,9 @@
all: deps build
.PHONY: deps
deps:
go get -d -v github.com/dustin/go-broadcast/...
.PHONY: build
build: deps
go build -o realtime-chat main.go rooms.go template.go

View File

@ -0,0 +1,51 @@
package main
import (
"fmt"
"io"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.Static("/", "./public")
router.POST("/upload", func(c *gin.Context) {
name := c.PostForm("name")
email := c.PostForm("email")
// Multipart form
form, err := c.MultipartForm()
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
return
}
files := form.File["files"]
for _, file := range files {
// Source
src, err := file.Open()
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("file open err: %s", err.Error()))
return
}
defer src.Close()
// Destination
dst, err := os.Create(file.Filename)
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("Create file err: %s", err.Error()))
return
}
defer dst.Close()
// Copy
io.Copy(dst, src)
}
c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email))
})
router.Run(":8080")
}

View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Multiple file upload</title>
</head>
<body>
<h1>Upload multiple files with fields</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Email: <input type="email" name="email"><br>
Files: <input type="file" name="files" multiple><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>

View File

@ -0,0 +1,46 @@
package main
import (
"fmt"
"io"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.Static("/", "./public")
router.POST("/upload", func(c *gin.Context) {
name := c.PostForm("name")
email := c.PostForm("email")
// Source
file, err := c.FormFile("file")
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
return
}
src, err := file.Open()
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("file open err: %s", err.Error()))
return
}
defer src.Close()
// Destination
dst, err := os.Create(file.Filename)
if err != nil {
c.String(http.StatusBadRequest, fmt.Sprintf("Create file err: %s", err.Error()))
return
}
defer dst.Close()
// Copy
io.Copy(dst, src)
c.String(http.StatusOK, fmt.Sprintf("File %s uploaded successfully with fields name=%s and email=%s.", file.Filename, name, email))
})
router.Run(":8080")
}

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Single file upload</title>
</head>
<body>
<h1>Upload single file with fields</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Email: <input type="email" name="email"><br>
Files: <input type="file" name="file"><br><br>
<input type="submit" value="Submit">
</form>
</body>

View File

@ -0,0 +1 @@
<h1>Hello {[{.name}]}</h1>

1
fixtures/basic/raw.tmpl Normal file
View File

@ -0,0 +1 @@
Date: {[{.now | formatAsDate}]}

21
fs.go
View File

@ -1,3 +1,7 @@
// Copyright 2017 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 (
@ -5,16 +9,15 @@ import (
"os"
)
type (
onlyfilesFS struct {
fs http.FileSystem
}
neuteredReaddirFile struct {
http.File
}
)
type onlyfilesFS struct {
fs http.FileSystem
}
// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used interally
type neuteredReaddirFile struct {
http.File
}
// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally
// in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns
// a filesystem that prevents http.FileServer() to list the directory files.

159
gin.go
View File

@ -15,10 +15,11 @@ import (
)
// Version is Framework's version
const Version = "v1.0rc2"
const Version = "v1.2"
var default404Body = []byte("404 page not found")
var default405Body = []byte("405 method not allowed")
var defaultAppEngine bool
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc
@ -32,54 +33,66 @@ func (c HandlersChain) Last() HandlerFunc {
return nil
}
type (
RoutesInfo []RouteInfo
RouteInfo struct {
Method string
Path string
Handler string
}
type RouteInfo struct {
Method string
Path string
Handler string
}
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
Engine struct {
RouterGroup
HTMLRender render.HTMLRender
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
trees methodTrees
type RoutesInfo []RouteInfo
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
// client is redirected to /foo with http status code 301 for GET requests
// and 307 for all other request methods.
RedirectTrailingSlash bool
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
RouterGroup
delims render.Delims
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
trees methodTrees
// If enabled, the router tries to fix the current request path, if no
// handle is registered for it.
// First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path.
// If a handle can be found for this route, the router makes a redirection
// to the corrected path with status code 301 for GET requests and 307 for
// all other request methods.
// For example /FOO and /..//Foo could be redirected to /foo.
// RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool
// Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
// client is redirected to /foo with http status code 301 for GET requests
// and 307 for all other request methods.
RedirectTrailingSlash bool
// If enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool
ForwardedByClientIP bool
}
)
// If enabled, the router tries to fix the current request path, if no
// handle is registered for it.
// First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path.
// If a handle can be found for this route, the router makes a redirection
// to the corrected path with status code 301 for GET requests and 307 for
// all other request methods.
// For example /FOO and /..//Foo could be redirected to /foo.
// RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool
// If enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
HandleMethodNotAllowed bool
ForwardedByClientIP bool
// #726 #755 If enabled, it will thrust some headers starting with
// 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool
// If enabled, the url.RawPath will be used to find parameters.
UseRawPath bool
// If true, the path value will be unescaped.
// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
// as url.Path gonna be used, which is already unescaped.
UnescapePathValues bool
}
var _ IRouter = &Engine{}
@ -89,6 +102,8 @@ var _ IRouter = &Engine{}
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
@ -97,11 +112,16 @@ func New() *Engine {
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
UnescapePathValues: true,
trees: make(methodTrees, 0, 9),
delims: render.Delims{"{{", "}}"},
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
@ -121,21 +141,26 @@ func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine}
}
func (engine *Engine) Delims(left, right string) *Engine {
engine.delims = render.Delims{left, right}
return engine
}
func (engine *Engine) LoadHTMLGlob(pattern string) {
if IsDebugging() {
debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern)))
engine.HTMLRender = render.HTMLDebug{Glob: pattern}
debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern)))
engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
} else {
templ := template.Must(template.ParseGlob(pattern))
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern))
engine.SetHTMLTemplate(templ)
}
}
func (engine *Engine) LoadHTMLFiles(files ...string) {
if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{Files: files}
engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims}
} else {
templ := template.Must(template.ParseFiles(files...))
templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFiles(files...))
engine.SetHTMLTemplate(templ)
}
}
@ -144,7 +169,12 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 {
debugPrintWARNINGSetHTMLTemplate()
}
engine.HTMLRender = render.HTMLProduction{Template: templ}
engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)}
}
func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
engine.FuncMap = funcMap
}
// NoRoute adds handlers for NoRoute. It return a 404 code by default.
@ -267,9 +297,26 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.pool.Put(c)
}
// Re-enter a context that has been rewritten.
// This can be done by setting c.Request.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
func (engine *Engine) handleHTTPRequest(context *Context) {
httpMethod := context.Request.Method
path := context.Request.URL.Path
var path string
var unescape bool
if engine.UseRawPath && len(context.Request.URL.RawPath) > 0 {
path = context.Request.URL.RawPath
unescape = engine.UnescapePathValues
} else {
path = context.Request.URL.Path
unescape = false
}
// Find root of the tree for the given HTTP method
t := engine.trees
@ -277,15 +324,15 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
if t[i].method == httpMethod {
root := t[i].root
// Find route in tree
handlers, params, tsr := root.getValue(path, context.Params)
handlers, params, tsr := root.getValue(path, context.Params, unescape)
if handlers != nil {
context.handlers = handlers
context.Params = params
context.Next()
context.writermem.WriteHeaderNow()
return
} else if httpMethod != "CONNECT" && path != "/" {
}
if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(context)
return
@ -302,7 +349,7 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method != httpMethod {
if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil {
if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
context.handlers = engine.allNoMethod
serveError(context, 405, default405Body)
return

View File

@ -1,4 +1,4 @@
#Gin Default Server
# Gin Default Server
This is API experiment for Gin.

View File

@ -1,3 +1,7 @@
// Copyright 2017 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 (
@ -6,18 +10,18 @@ import (
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"net/http/httptest"
)
func testRequest(t *testing.T, url string) {
resp, err := http.Get(url)
defer resp.Body.Close()
assert.NoError(t, err)
defer resp.Body.Close()
body, ioerr := ioutil.ReadAll(resp.Body)
assert.NoError(t, ioerr)
@ -115,17 +119,17 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
testRequest(t, ts.URL+"/example")
}
func TestWithHttptestWithSpecifiedPort(t *testing.T) {
router := New()
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
// func TestWithHttptestWithSpecifiedPort(t *testing.T) {
// router := New()
// router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
l, _ := net.Listen("tcp", ":8033")
ts := httptest.Server{
Listener: l,
Config: &http.Server{Handler: router},
}
ts.Start()
defer ts.Close()
// l, _ := net.Listen("tcp", ":8033")
// ts := httptest.Server{
// Listener: l,
// Config: &http.Server{Handler: router},
// }
// ts.Start()
// defer ts.Close()
testRequest(t, "http://localhost:8033/example")
}
// testRequest(t, "http://localhost:8033/example")
// }

View File

@ -5,14 +5,98 @@
package gin
import (
"fmt"
"html/template"
"io/ioutil"
"net/http"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func formatAsDate(t time.Time) string {
year, month, day := t.Date()
return fmt.Sprintf("%d/%02d/%02d", year, month, day)
}
func setupHTMLFiles(t *testing.T) func() {
go func() {
SetMode(TestMode)
router := New()
router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
router.LoadHTMLFiles("./fixtures/basic/hello.tmpl", "./fixtures/basic/raw.tmpl")
router.GET("/test", func(c *Context) {
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
})
router.GET("/raw", func(c *Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
router.Run(":8888")
}()
t.Log("waiting 1 second for server startup")
time.Sleep(1 * time.Second)
return func() {}
}
func setupHTMLGlob(t *testing.T) func() {
go func() {
SetMode(DebugMode)
router := New()
router.Delims("{[{", "}]}")
router.SetFuncMap(template.FuncMap{
"formatAsDate": formatAsDate,
})
router.LoadHTMLGlob("./fixtures/basic/*")
router.GET("/test", func(c *Context) {
c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
})
router.GET("/raw", func(c *Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
})
})
router.Run(":8888")
}()
t.Log("waiting 1 second for server startup")
time.Sleep(1 * time.Second)
return func() {}
}
//TODO
// func (engine *Engine) LoadHTMLGlob(pattern string) {
func TestLoadHTMLGlob(t *testing.T) {
td := setupHTMLGlob(t)
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 TestLoadHTMLGlobFromFuncMap(t *testing.T) {
time.Now()
td := setupHTMLGlob(t)
res, err := http.Get("http://127.0.0.1:8888/raw")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp[:]))
td()
}
// func (engine *Engine) LoadHTMLFiles(files ...string) {
// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
@ -42,6 +126,32 @@ func TestCreateEngine(t *testing.T) {
// SetMode(TestMode)
// }
func TestLoadHTMLFiles(t *testing.T) {
td := setupHTMLFiles(t)
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 TestLoadHTMLFilesFuncMap(t *testing.T) {
time.Now()
td := setupHTMLFiles(t)
res, err := http.Get("http://127.0.0.1:8888/raw")
if err != nil {
fmt.Println(err)
}
resp, _ := ioutil.ReadAll(res.Body)
assert.Equal(t, "Date: 2017/07/01\n", string(resp[:]))
td()
}
func TestLoadHTMLReleaseMode(t *testing.T) {
}

View File

@ -1,14 +0,0 @@
package gin
import (
"net/http/httptest"
)
func CreateTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) {
w = httptest.NewRecorder()
r = New()
c = r.allocateContext()
c.reset()
c.writermem.reset(w)
return
}

View File

@ -14,20 +14,28 @@ import (
)
var (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109})
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109})
disableColor = false
)
// DisableConsoleColor disables color output in the console
func DisableConsoleColor() {
disableColor = true
}
// ErrorLogger returns a handlerfunc for any error type
func ErrorLogger() HandlerFunc {
return ErrorLoggerT(ErrorTypeAny)
}
// ErrorLoggerT returns a handlerfunc for a given error type
func ErrorLoggerT(typ ErrorType) HandlerFunc {
return func(c *Context) {
c.Next()
@ -49,7 +57,9 @@ func Logger() HandlerFunc {
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
isTerm := true
if w, ok := out.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) {
if w, ok := out.(*os.File); !ok ||
(os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) ||
disableColor {
isTerm = false
}
@ -87,12 +97,12 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
}
comment := c.Errors.ByType(ErrorTypePrivate).String()
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s",
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %s %-7s %s\n%s",
end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, reset,
latency,
clientIP,
methodColor, reset, method,
methodColor, method, reset,
path,
comment,
)

View File

@ -36,37 +36,43 @@ func TestLogger(t *testing.T) {
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
performRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
@ -107,16 +113,16 @@ func TestErrorLogger(t *testing.T) {
})
w := performRequest(router, "GET", "/error")
assert.Equal(t, w.Code, 200)
assert.Equal(t, w.Body.String(), "{\"error\":\"this is an error\"}\n")
assert.Equal(t, 200, w.Code)
assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort")
assert.Equal(t, w.Code, 401)
assert.Equal(t, w.Body.String(), "{\"error\":\"no authorized\"}\n")
assert.Equal(t, 401, w.Code)
assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print")
assert.Equal(t, w.Code, 500)
assert.Equal(t, w.Body.String(), "hola!{\"error\":\"this is an error\"}\n")
assert.Equal(t, 500, w.Code)
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
}
func TestSkippingPaths(t *testing.T) {
@ -129,6 +135,14 @@ func TestSkippingPaths(t *testing.T) {
performRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200")
buffer.Reset()
performRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "")
}
func TestDisableConsoleColor(t *testing.T) {
New()
assert.False(t, disableColor)
DisableConsoleColor()
assert.True(t, disableColor)
}

BIN
logo.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -7,10 +7,9 @@ package gin
import (
"errors"
"strings"
"testing"
"github.com/manucorporat/sse"
"github.com/gin-contrib/sse"
"github.com/stretchr/testify/assert"
)
@ -245,6 +244,6 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 400)
assert.Equal(t, strings.Replace(w.Body.String(), " ", "", -1), strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1))
assert.Equal(t, 400, 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))
}

View File

@ -19,9 +19,9 @@ const (
TestMode string = "test"
)
const (
debugCode = iota
releaseCode
testCode
debugCode = iota
releaseCode
testCode
)
// DefaultWriter is the default io.Writer used the Gin for debug output and

View File

@ -1,11 +1,11 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Based on the path package, Copyright 2009 The Go Authors.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE.
package gin
// CleanPath is the URL version of path.Clean, it returns a canonical URL path
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
// for p, eliminating . and .. elements.
//
// The following rules are applied iteratively until no further processing can

View File

@ -1,7 +1,7 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Based on the path package, Copyright 2009 The Go Authors.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
package gin

View File

@ -11,10 +11,13 @@ type Data struct {
Data []byte
}
func (r Data) Render(w http.ResponseWriter) error {
if len(r.ContentType) > 0 {
w.Header()["Content-Type"] = []string{r.ContentType}
}
w.Write(r.Data)
return nil
// Render (Data) writes data with custom ContentType
func (r Data) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
_, err = w.Write(r.Data)
return
}
func (r Data) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.ContentType})
}

View File

@ -10,17 +10,25 @@ import (
)
type (
Delims struct {
Left string
Right string
}
HTMLRender interface {
Instance(string, interface{}) Render
}
HTMLProduction struct {
Template *template.Template
Delims Delims
}
HTMLDebug struct {
Files []string
Glob string
Files []string
Glob string
Delims Delims
FuncMap template.FuncMap
}
HTML struct {
@ -48,19 +56,27 @@ func (r HTMLDebug) Instance(name string, data interface{}) Render {
}
}
func (r HTMLDebug) loadTemplate() *template.Template {
if r.FuncMap == nil {
r.FuncMap = template.FuncMap{}
}
if len(r.Files) > 0 {
return template.Must(template.ParseFiles(r.Files...))
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFiles(r.Files...))
}
if len(r.Glob) > 0 {
return template.Must(template.ParseGlob(r.Glob))
return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
}
panic("the HTML debug render was created without files or glob pattern")
}
func (r HTML) Render(w http.ResponseWriter) error {
writeContentType(w, htmlContentType)
r.WriteContentType(w)
if len(r.Name) == 0 {
return r.Template.Execute(w, r.Data)
}
return r.Template.ExecuteTemplate(w, r.Name, r.Data)
}
func (r HTML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, htmlContentType)
}

View File

@ -21,12 +21,29 @@ type (
var jsonContentType = []string{"application/json; charset=utf-8"}
func (r JSON) Render(w http.ResponseWriter) error {
return WriteJSON(w, r.Data)
func (r JSON) Render(w http.ResponseWriter) (err error) {
if err = WriteJSON(w, r.Data); err != nil {
panic(err)
}
return
}
func (r JSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
}
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj)
if err != nil {
return err
}
w.Write(jsonBytes)
return nil
}
func (r IndentedJSON) Render(w http.ResponseWriter) error {
writeContentType(w, jsonContentType)
r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
if err != nil {
return err
@ -35,7 +52,6 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error {
return nil
}
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
return json.NewEncoder(w).Encode(obj)
}

31
render/msgpack.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2017 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 render
import (
"net/http"
"github.com/ugorji/go/codec"
)
type MsgPack struct {
Data interface{}
}
var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
func (r MsgPack) WriteContentType(w http.ResponseWriter) {
writeContentType(w, msgpackContentType)
}
func (r MsgPack) Render(w http.ResponseWriter) error {
return WriteMsgPack(w, r.Data)
}
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)
}

View File

@ -22,3 +22,5 @@ func (r Redirect) Render(w http.ResponseWriter) error {
http.Redirect(w, r.Request, r.Location, r.Code)
return nil
}
func (r Redirect) WriteContentType(http.ResponseWriter) {}

View File

@ -8,6 +8,7 @@ import "net/http"
type Render interface {
Render(http.ResponseWriter) error
WriteContentType(w http.ResponseWriter)
}
var (
@ -21,6 +22,8 @@ var (
_ HTMLRender = HTMLDebug{}
_ HTMLRender = HTMLProduction{}
_ Render = YAML{}
_ Render = MsgPack{}
_ Render = MsgPack{}
)
func writeContentType(w http.ResponseWriter, value []string) {

View File

@ -5,17 +5,40 @@
package render
import (
"bytes"
"encoding/xml"
"html/template"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
)
// TODO unit tests
// test errors
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
"foo": "bar",
}
err := (MsgPack{data}).Render(w)
assert.NoError(t, err)
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err = codec.NewEncoder(buf, h).Encode(data)
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")
}
func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
@ -25,8 +48,8 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w)
assert.NoError(t, err)
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderIndentedJSON(t *testing.T) {

View File

@ -22,9 +22,12 @@ func (r String) Render(w http.ResponseWriter) error {
return nil
}
func (r String) WriteContentType(w http.ResponseWriter) {
writeContentType(w, plainContentType)
}
func WriteString(w http.ResponseWriter, format string, data []interface{}) {
writeContentType(w, plainContentType)
if len(data) > 0 {
fmt.Fprintf(w, format, data...)
} else {

View File

@ -16,6 +16,10 @@ type XML struct {
var xmlContentType = []string{"application/xml; charset=utf-8"}
func (r XML) Render(w http.ResponseWriter) error {
writeContentType(w, xmlContentType)
r.WriteContentType(w)
return xml.NewEncoder(w).Encode(r.Data)
}
func (r XML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, xmlContentType)
}

View File

@ -17,7 +17,7 @@ type YAML struct {
var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
func (r YAML) Render(w http.ResponseWriter) error {
writeContentType(w, yamlContentType)
r.WriteContentType(w)
bytes, err := yaml.Marshal(r.Data)
if err != nil {
@ -27,3 +27,7 @@ func (r YAML) Render(w http.ResponseWriter) error {
w.Write(bytes)
return nil
}
func (r YAML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, yamlContentType)
}

View File

@ -16,36 +16,34 @@ const (
defaultStatus = 200
)
type (
ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier
type ResponseWriter interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier
// Returns the HTTP response status code of the current request.
Status() int
// Returns the HTTP response status code of the current request.
Status() int
// Returns the number of bytes already written into the response http body.
// See Written()
Size() int
// Returns the number of bytes already written into the response http body.
// See Written()
Size() int
// Writes the string into the response body.
WriteString(string) (int, error)
// Writes the string into the response body.
WriteString(string) (int, error)
// Returns true if the response body was already written.
Written() bool
// Returns true if the response body was already written.
Written() bool
// Forces to write the http header (status code + headers).
WriteHeaderNow()
}
// Forces to write the http header (status code + headers).
WriteHeaderNow()
}
responseWriter struct {
http.ResponseWriter
size int
status int
}
)
type responseWriter struct {
http.ResponseWriter
size int
status int
}
var _ ResponseWriter = &responseWriter{}

View File

@ -11,39 +11,37 @@ import (
"strings"
)
type (
IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}
type IRouter interface {
IRoutes
Group(string, ...HandlerFunc) *RouterGroup
}
IRoutes interface {
Use(...HandlerFunc) IRoutes
type IRoutes interface {
Use(...HandlerFunc) IRoutes
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
StaticFile(string, string) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
StaticFile(string, string) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
// RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix
// and an array of handlers (middleware)
RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
)
// RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix
// and an array of handlers (middleware)
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
var _ IRouter = &RouterGroup{}

View File

@ -400,3 +400,42 @@ func TestRouterNotFound(t *testing.T) {
w = performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 404)
}
func TestRouteRawPath(t *testing.T) {
route := New()
route.UseRawPath = true
route.POST("/project/:name/build/:num", func(c *Context) {
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, "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)
}
func TestRouteRawPathNoUnescape(t *testing.T) {
route := New()
route.UseRawPath = true
route.UnescapePathValues = false
route.POST("/project/:name/build/:num", func(c *Context) {
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, "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)
}

18
test_helpers.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright 2017 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 (
"net/http"
)
// CreateTestContext returns a fresh engine and context for testing purposes
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
r = New()
c = r.allocateContext()
c.reset()
c.writermem.reset(w)
return
}

31
tree.go
View File

@ -1,10 +1,11 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
package gin
import (
"net/url"
"strings"
"unicode"
)
@ -104,9 +105,7 @@ func (n *node) incrementChildPrio(pos int) int {
newPos := pos
for newPos > 0 && n.children[newPos-1].priority < prio {
// swap node positions
tmpN := n.children[newPos-1]
n.children[newPos-1] = n.children[newPos]
n.children[newPos] = tmpN
n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]
newPos--
}
@ -363,7 +362,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
func (n *node) getValue(path string, po Params) (handlers HandlersChain, p Params, tsr bool) {
func (n *node) getValue(path string, po Params, unescape bool) (handlers HandlersChain, p Params, tsr bool) {
p = po
walk: // Outer loop for walking the tree
for {
@ -406,7 +405,15 @@ walk: // Outer loop for walking the tree
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[1:]
p[i].Value = path[:end]
val := path[:end]
if unescape {
var err error
if p[i].Value, err = url.QueryUnescape(val); err != nil {
p[i].Value = val // fallback, in case of error
}
} else {
p[i].Value = val
}
// we need to go deeper!
if end < len(path) {
@ -423,7 +430,8 @@ walk: // Outer loop for walking the tree
if handlers = n.handlers; handlers != nil {
return
} else if len(n.children) == 1 {
}
if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
@ -440,7 +448,14 @@ walk: // Outer loop for walking the tree
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[2:]
p[i].Value = path
if unescape {
var err error
if p[i].Value, err = url.QueryUnescape(path); err != nil {
p[i].Value = path // fallback, in case of error
}
} else {
p[i].Value = path
}
handlers = n.handlers
return

View File

@ -1,6 +1,6 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
// in the LICENSE file.
// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
package gin
@ -37,9 +37,14 @@ type testRequests []struct {
ps Params
}
func checkRequests(t *testing.T, tree *node, requests testRequests) {
func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
unescape := false
if len(unescapes) >= 1 {
unescape = unescapes[0]
}
for _, request := range requests {
handler, ps, _ := tree.getValue(request.path, nil)
handler, ps, _ := tree.getValue(request.path, nil, unescape)
if handler == nil {
if !request.nilHandler {
@ -197,6 +202,45 @@ func TestTreeWildcard(t *testing.T) {
checkMaxParams(t, tree)
}
func TestUnescapeParameters(t *testing.T) {
tree := &node{}
routes := [...]string{
"/",
"/cmd/:tool/:sub",
"/cmd/:tool/",
"/src/*filepath",
"/search/:query",
"/files/:dir/*filepath",
"/info/:user/project/:project",
"/info/:user",
}
for _, route := range routes {
tree.addRoute(route, fakeHandler(route))
}
//printChildren(tree, "")
unescape := true
checkRequests(t, tree, testRequests{
{"/", false, "/", nil},
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}},
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}},
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}},
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}},
{"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}},
{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
}, unescape)
checkPriorities(t, tree)
checkMaxParams(t, tree)
}
func catchPanic(testFunc func()) (recv interface{}) {
defer func() {
recv = recover()
@ -430,7 +474,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/doc/",
}
for _, route := range tsrRoutes {
handler, _, tsr := tree.getValue(route, nil)
handler, _, tsr := tree.getValue(route, nil, false)
if handler != nil {
t.Fatalf("non-nil handler for TSR route '%s", route)
} else if !tsr {
@ -447,7 +491,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/api/world/abc",
}
for _, route := range noTsrRoutes {
handler, _, tsr := tree.getValue(route, nil)
handler, _, tsr := tree.getValue(route, nil, false)
if handler != nil {
t.Fatalf("non-nil handler for No-TSR route '%s", route)
} else if tsr {
@ -466,7 +510,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
t.Fatalf("panic inserting test route: %v", recv)
}
handler, _, tsr := tree.getValue("/", nil)
handler, _, tsr := tree.getValue("/", nil, false)
if handler != nil {
t.Fatalf("non-nil handler")
} else if tsr {
@ -617,7 +661,7 @@ func TestTreeInvalidNodeType(t *testing.T) {
// normal lookup
recv := catchPanic(func() {
tree.getValue("/test", nil)
tree.getValue("/test", nil, false)
})
if rs, ok := recv.(string); !ok || rs != panicMsg {
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)

109
vendor/vendor.json vendored Normal file
View File

@ -0,0 +1,109 @@
{
"comment": "v1.2",
"ignore": "test",
"package": [
{
"checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=",
"comment": "v1.1.0",
"path": "github.com/davecgh/go-spew/spew",
"revision": "346938d642f2ec3594ed81d874461961cd0faa76",
"revisionTime": "2016-10-29T20:57:26Z"
},
{
"checksumSHA1": "7c3FuEadBInl/4ExSrB7iJMXpe4=",
"path": "github.com/dustin/go-broadcast",
"revision": "3bdf6d4a7164a50bc19d5f230e2981d87d2584f1",
"revisionTime": "2014-06-27T04:00:55Z"
},
{
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
"path": "github.com/gin-contrib/sse",
"revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae",
"revisionTime": "2017-01-09T09:34:21Z"
},
{
"checksumSHA1": "FJKrZuFmeLJp8HDeJc6UkIDBPUw=",
"path": "github.com/gin-gonic/autotls",
"revision": "5b3297bdcee778ff3bbdc99ab7c41e1c2677d22d",
"revisionTime": "2017-04-16T09:39:34Z"
},
{
"checksumSHA1": "qlPUeFabwF4RKAOF1H+yBFU1Veg=",
"path": "github.com/golang/protobuf/proto",
"revision": "5a0f697c9ed9d68fef0116532c6e05cfeae00e55",
"revisionTime": "2017-06-01T23:02:30Z"
},
{
"checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=",
"path": "github.com/manucorporat/stats",
"revision": "8f2d6ace262eba462e9beb552382c98be51d807b",
"revisionTime": "2015-05-31T20:46:25Z"
},
{
"checksumSHA1": "U6lX43KDDlNOn+Z0Yyww+ZzHfFo=",
"path": "github.com/mattn/go-isatty",
"revision": "57fdcb988a5c543893cc61bce354a6e24ab70022",
"revisionTime": "2017-03-07T16:30:44Z"
},
{
"checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
"comment": "v1.0.0",
"path": "github.com/pmezard/go-difflib/difflib",
"revision": "792786c7400a136282c1664665ae0a8db921c6c2",
"revisionTime": "2016-01-10T10:55:54Z"
},
{
"checksumSHA1": "Q2V7Zs3diLmLfmfbiuLpSxETSuY=",
"comment": "v1.1.4",
"path": "github.com/stretchr/testify/assert",
"revision": "976c720a22c8eb4eb6a0b4348ad85ad12491a506",
"revisionTime": "2016-09-25T22:06:09Z"
},
{
"checksumSHA1": "CoxdaTYdPZNJXr8mJfLxye428N0=",
"path": "github.com/ugorji/go/codec",
"revision": "c88ee250d0221a57af388746f5cf03768c21d6e2",
"revisionTime": "2017-02-15T20:11:44Z"
},
{
"checksumSHA1": "W0j4I7QpxXlChjyhAojZmFjU6Bg=",
"path": "golang.org/x/crypto/acme",
"revision": "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d",
"revisionTime": "2017-06-19T06:03:41Z"
},
{
"checksumSHA1": "TrKJW+flz7JulXU3sqnBJjGzgQc=",
"path": "golang.org/x/crypto/acme/autocert",
"revision": "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d",
"revisionTime": "2017-06-19T06:03:41Z"
},
{
"checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
"comment": "release-branch.go1.7",
"path": "golang.org/x/net/context",
"revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a",
"revisionTime": "2016-10-18T08:54:36Z"
},
{
"checksumSHA1": "TVEkpH3gq84iQ39I4R+mlDwjuVI=",
"path": "golang.org/x/sys/unix",
"revision": "99f16d856c9836c42d24e7ab64ea72916925fa97",
"revisionTime": "2017-03-08T15:04:45Z"
},
{
"checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=",
"comment": "v8.18.1",
"path": "gopkg.in/go-playground/validator.v8",
"revision": "5f57d2222ad794d0dffb07e664ea05e2ee07d60c",
"revisionTime": "2016-07-18T13:41:25Z"
},
{
"checksumSHA1": "12GqsW8PiRPnezDDy0v4brZrndM=",
"comment": "v2",
"path": "gopkg.in/yaml.v2",
"revision": "a5b47d31c556af34a302ce5d659e6fea44d90de0",
"revisionTime": "2016-09-28T15:37:09Z"
}
],
"rootPath": "github.com/gin-gonic/gin"
}