Merge pull request #871 from gin-gonic/develop

v1.2
This commit is contained in:
Javier Provecho Fernandez 2017-07-02 11:28:26 +02:00 committed by GitHub
commit d459835d2b
66 changed files with 2200 additions and 331 deletions

4
.gitignore vendored
View File

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

View File

@ -1,14 +1,23 @@
language: go language: go
sudo: false sudo: false
go: go:
- 1.4 - 1.6.x
- 1.5.4 - 1.7.x
- 1.6.4 - 1.8.x
- 1.7.4 - master
- tip
git:
depth: 3
install:
- make install
script: script:
- go test -v -covermode=count -coverprofile=coverage.out - make vet
- make fmt-check
- make embedmd
- make misspell-check
- make test
after_success: after_success:
- bash <(curl -s https://codecov.io/bash) - 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. 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) **Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
@ -226,4 +224,4 @@ People and companies, who have contributed, in alphabetical order.
**@yuyabee** **@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 BenchmarkR2router_GPlusAll 100000 16251 ns/op 5040 B/op 76 allocs/op
BenchmarkRevel_GPlusAll 20000 93489 ns/op 21656 B/op 368 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 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] Fast path for writing Content-Type.
- [PERFORMANCE] Much faster 404 routing - [PERFORMANCE] Much faster 404 routing
@ -35,7 +76,7 @@
- [FIX] MIT license in every file - [FIX] MIT license in every file
###Gin 1.0rc1 (May 22, 2015) ### Gin 1.0rc1 (May 22, 2015)
- [PERFORMANCE] Zero allocation router - [PERFORMANCE] Zero allocation router
- [PERFORMANCE] Faster JSON, XML and text rendering - [PERFORMANCE] Faster JSON, XML and text rendering
@ -79,7 +120,7 @@
- [FIX] Better support for Google App Engine (using log instead of fmt) - [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] Support multipart/form-data
- [NEW] NoMethod handler - [NEW] NoMethod handler
@ -89,14 +130,14 @@
- [FIX] Improve color logger - [FIX] Improve color logger
###Gin 0.5 (Feb 7, 2015) ### Gin 0.5 (Feb 7, 2015)
- [NEW] Content Negotiation - [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip - [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding - [FIX] Fix unexported/ignored fields in binding
###Gin 0.4 (Aug 21, 2014) ### Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode - [NEW] Development mode
- [NEW] Unit tests - [NEW] Unit tests
@ -105,7 +146,7 @@
- [FIX] Improved documentation for model binding - [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] Normal log and error log are printed in the same call.
- [PERFORMANCE] Improve performance of NoRouter() - [PERFORMANCE] Improve performance of NoRouter()
@ -123,7 +164,7 @@
- [FIX] Check application/x-www-form-urlencoded when parsing form - [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 - [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
- [NEW] Travis CI integration - [NEW] Travis CI integration
- [NEW] Completely new logger - [NEW] Completely new logger

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)

341
README.md
View File

@ -1,4 +1,3 @@
# Gin Web Framework # Gin Web Framework
<img align="right" src="https://raw.githubusercontent.com/gin-gonic/gin/master/logo.jpg"> <img align="right" src="https://raw.githubusercontent.com/gin-gonic/gin/master/logo.jpg">
@ -16,10 +15,11 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
```sh ```sh
$ cat test.go $ cat test.go
``` ```
```go ```go
package main package main
import "gopkg.in/gin-gonic/gin.v1" import "github.com/gin-gonic/gin"
func main() { func main() {
r := gin.Default() r := gin.Default()
@ -87,28 +87,31 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648
1. Download and install it: 1. Download and install it:
```sh ```sh
$ go get gopkg.in/gin-gonic/gin.v1 $ go get github.com/gin-gonic/gin
``` ```
2. Import it in your code: 2. Import it in your code:
```go ```go
import "gopkg.in/gin-gonic/gin.v1" import "github.com/gin-gonic/gin"
``` ```
3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`. 3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
```go ```go
import "net/http" import "net/http"
``` ```
## API Examples ## API Examples
#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS ### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
```go ```go
func main() { func main() {
// Disable Console Color
// gin.DisableConsoleColor()
// Creates a gin router with default middleware: // Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware // logger and recovery (crash-free) middleware
router := gin.Default() router := gin.Default()
@ -128,7 +131,7 @@ func main() {
} }
``` ```
#### Parameters in path ### Parameters in path
```go ```go
func main() { func main() {
@ -153,7 +156,8 @@ func main() {
} }
``` ```
#### Querystring parameters ### Querystring parameters
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
@ -220,34 +224,66 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great 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 ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
// single file
file, _ := c.FormFile("file")
log.Println(file.Filename)
file, header , err := c.Request.FormFile("upload") c.String(http.StatusOK, fmt.Printf("'%s' uploaded!", file.Filename))
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)
}
}) })
router.Run(":8080") 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 ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
@ -272,14 +308,14 @@ func main() {
} }
``` ```
### Blank Gin without middleware by default
#### Blank Gin without middleware by default
Use Use
```go ```go
r := gin.New() r := gin.New()
``` ```
instead of instead of
```go ```go
@ -287,7 +323,7 @@ r := gin.Default()
``` ```
#### Using middleware ### Using middleware
```go ```go
func main() { func main() {
// Creates a router without any middleware by default // Creates a router without any middleware by default
@ -322,7 +358,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). 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).
@ -372,13 +408,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 ```go
package main package main
import ( import (
"gopkg.in/gin-gonic/gin.v1" "github.com/gin-gonic/gin"
) )
type LoginForm struct { type LoginForm struct {
@ -390,7 +461,7 @@ func main() {
router := gin.Default() router := gin.Default()
router.POST("/login", func(c *gin.Context) { router.POST("/login", func(c *gin.Context) {
// you can bind multipart form with explicit binding declaration: // 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: // or you can simply use autobinding with Bind method:
var form LoginForm var form LoginForm
// in this case proper binding will be automatically selected // in this case proper binding will be automatically selected
@ -411,8 +482,7 @@ Test it with:
$ curl -v --form user=user --form password=password http://localhost:8080/login $ curl -v --form user=user --form password=password http://localhost:8080/login
``` ```
### XML, JSON and YAML rendering
#### XML, JSON and YAML rendering
```go ```go
func main() { func main() {
@ -451,7 +521,7 @@ func main() {
} }
``` ```
####Serving static files ### Serving static files
```go ```go
func main() { func main() {
@ -465,9 +535,9 @@ func main() {
} }
``` ```
####HTML rendering ### HTML rendering
Using LoadHTMLTemplates() Using LoadHTMLGlob() or LoadHTMLFiles()
```go ```go
func main() { func main() {
@ -482,7 +552,9 @@ func main() {
router.Run(":8080") router.Run(":8080")
} }
``` ```
templates/index.tmpl templates/index.tmpl
```html ```html
<html> <html>
<h1> <h1>
@ -510,7 +582,9 @@ func main() {
router.Run(":8080") router.Run(":8080")
} }
``` ```
templates/posts/index.tmpl templates/posts/index.tmpl
```html ```html
{{ define "posts/index.tmpl" }} {{ define "posts/index.tmpl" }}
<html><h1> <html><h1>
@ -520,7 +594,9 @@ templates/posts/index.tmpl
</html> </html>
{{ end }} {{ end }}
``` ```
templates/users/index.tmpl templates/users/index.tmpl
```html ```html
{{ define "users/index.tmpl" }} {{ define "users/index.tmpl" }}
<html><h1> <html><h1>
@ -544,8 +620,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: Issuing a HTTP redirect is easy:
@ -557,7 +684,7 @@ r.GET("/test", func(c *gin.Context) {
Both internal and external locations are supported. Both internal and external locations are supported.
#### Custom Middleware ### Custom Middleware
```go ```go
func Logger() gin.HandlerFunc { func Logger() gin.HandlerFunc {
@ -597,7 +724,8 @@ func main() {
} }
``` ```
#### Using BasicAuth() middleware ### Using BasicAuth() middleware
```go ```go
// simulate some private data // simulate some private data
var secrets = gin.H{ var secrets = gin.H{
@ -635,8 +763,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. 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 ```go
@ -668,7 +796,7 @@ func main() {
} }
``` ```
#### Custom HTTP configuration ### Custom HTTP configuration
Use `http.ListenAndServe()` directly, like this: Use `http.ListenAndServe()` directly, like this:
@ -695,7 +823,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? Do you want to graceful restart or stop your web server?
There are some ways this can be done. There are some ways this can be done.
@ -712,6 +899,62 @@ endless.ListenAndServe(":4242", router)
An alternative to endless: An alternative to endless:
* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully. * [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.
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.
[embedmd]:# (examples/graceful-shutdown/graceful-shutdown/server.go go)
```go
// +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")
}
```
## Contributing ## Contributing
@ -726,7 +969,7 @@ An alternative to endless:
- You should add/modify tests to cover your proposed code changes. - 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. - If your pull request contains a new feature, please document it on the README.
## Example ## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.

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 package gin
import ( import (

View File

@ -15,6 +15,8 @@ const (
MIMEPOSTForm = "application/x-www-form-urlencoded" MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data" MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf" MIMEPROTOBUF = "application/x-protobuf"
MIMEMSGPACK = "application/x-msgpack"
MIMEMSGPACK2 = "application/msgpack"
) )
type Binding interface { type Binding interface {
@ -40,22 +42,25 @@ var (
FormPost = formPostBinding{} FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{} FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{} ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
) )
func Default(method, contentType string) Binding { func Default(method, contentType string) Binding {
if method == "GET" { if method == "GET" {
return Form return Form
} else { }
switch contentType {
case MIMEJSON: switch contentType {
return JSON case MIMEJSON:
case MIMEXML, MIMEXML2: return JSON
return XML case MIMEXML, MIMEXML2:
case MIMEPROTOBUF: return XML
return ProtoBuf case MIMEPROTOBUF:
default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: return ProtoBuf
return Form 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/gin-gonic/gin/binding/example"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/ugorji/go/codec"
) )
type FooStruct struct { 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 { type FooBarStruct struct {
FooStruct 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) { 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("POST", MIMEPROTOBUF), ProtoBuf)
assert.Equal(t, Default("PUT", 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) { func TestBindingJSON(t *testing.T) {
@ -121,6 +124,26 @@ func TestBindingProtoBuf(t *testing.T) {
string(data), string(data[1:])) 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) { func TestValidationFails(t *testing.T) {
var obj FooStruct var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`) 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) 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) { func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return 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 package binding
import ( import (

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"reflect" "reflect"
"strconv" "strconv"
"time"
) )
func mapForm(ptr interface{}, form map[string][]string) error { 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) val.Field(i).Set(slice)
} else { } 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 { if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err return err
} }
@ -140,6 +147,31 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
return err 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: // 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/issues/40
// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 // https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659

View File

@ -6,7 +6,6 @@ package binding
import ( import (
"encoding/json" "encoding/json"
"net/http" "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 package binding
import ( import (
"github.com/golang/protobuf/proto"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"github.com/golang/protobuf/proto"
) )
type protobufBinding struct{} type protobufBinding struct{}

View File

@ -7,17 +7,18 @@ package gin
import ( import (
"errors" "errors"
"io" "io"
"io/ioutil"
"math" "math"
"mime/multipart"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time" "time"
"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
"github.com/manucorporat/sse"
"golang.org/x/net/context"
) )
// Content-Type MIME of the most common data formats // Content-Type MIME of the most common data formats
@ -31,7 +32,10 @@ const (
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm 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, // Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example. // manage the flow, validate the JSON of a request and render a JSON response for example.
@ -50,8 +54,6 @@ type Context struct {
Accepted []string Accepted []string
} }
var _ context.Context = &Context{}
/************************************/ /************************************/
/********** CONTEXT CREATION ********/ /********** 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. // 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 { func (c *Context) Copy() *Context {
var cp = *c var cp = *c
cp.writermem.ResponseWriter = nil cp.writermem.ResponseWriter = nil
@ -83,13 +85,18 @@ func (c *Context) HandlerName() string {
return nameOfFunction(c.handlers.Last()) return nameOfFunction(c.handlers.Last())
} }
// Handler returns the main handler.
func (c *Context) Handler() HandlerFunc {
return c.handlers.Last()
}
/************************************/ /************************************/
/*********** FLOW CONTROL ***********/ /*********** FLOW CONTROL ***********/
/************************************/ /************************************/
// Next should be used only inside middleware. // Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler. // It executes the pending handlers in the chain inside the calling handler.
// See example in github. // See example in GitHub.
func (c *Context) Next() { func (c *Context) Next() {
c.index++ c.index++
s := int8(len(c.handlers)) 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. // 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) { func (c *Context) AbortWithStatus(code int) {
c.Status(code) c.Status(code)
c.Writer.WriteHeaderNow() c.Writer.WriteHeaderNow()
c.Abort() 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 // AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and
// pushes the specified error to `c.Errors`. // pushes the specified error to `c.Errors`.
// See Context.Error() for more details. // See Context.Error() for more details.
@ -154,7 +168,7 @@ func (c *Context) Error(err error) *Error {
/******** METADATA MANAGEMENT********/ /******** 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. // It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) { func (c *Context) Set(key string, value interface{}) {
if c.Keys == nil { if c.Keys == nil {
@ -166,9 +180,7 @@ func (c *Context) Set(key string, value interface{}) {
// Get returns the value for the given key, ie: (value, true). // Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false) // If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) { 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 return
} }
@ -180,6 +192,94 @@ func (c *Context) MustGet(key string) interface{} {
panic("Key \"" + key + "\" does not exist") 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 ************/ /************ INPUT DATA ************/
/************************************/ /************************************/
@ -195,7 +295,7 @@ func (c *Context) Param(key string) string {
} }
// Query returns the keyed url query value if it exists, // 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)` // It is shortcut for `c.Request.URL.Query().Get(key)`
// GET /path?id=1234&name=Manu&value= // GET /path?id=1234&name=Manu&value=
// c.Query("id") == "1234" // c.Query("id") == "1234"
@ -208,7 +308,7 @@ func (c *Context) Query(key string) string {
} }
// DefaultQuery returns the keyed url query value if it exists, // 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. // See: Query() and GetQuery() for further information.
// GET /?name=Manu&lastname= // GET /?name=Manu&lastname=
// c.DefaultQuery("name", "unknown") == "Manu" // c.DefaultQuery("name", "unknown") == "Manu"
@ -223,7 +323,7 @@ func (c *Context) DefaultQuery(key, defaultValue string) string {
// GetQuery is like Query(), it returns the keyed url query value // GetQuery is like Query(), it returns the keyed url query value
// if it exists `(value, true)` (even when the value is an empty string), // 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)` // It is shortcut for `c.Request.URL.Query().Get(key)`
// GET /?name=Manu&lastname= // GET /?name=Manu&lastname=
// ("Manu", true) == c.GetQuery("name") // ("Manu", true) == c.GetQuery("name")
@ -296,7 +396,7 @@ func (c *Context) PostFormArray(key string) []string {
func (c *Context) GetPostFormArray(key string) ([]string, bool) { func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request req := c.Request
req.ParseForm() req.ParseForm()
req.ParseMultipartForm(32 << 20) // 32 MB req.ParseMultipartForm(defaultMemory)
if values := req.PostForm[key]; len(values) > 0 { if values := req.PostForm[key]; len(values) > 0 {
return values, true return values, true
} }
@ -308,6 +408,18 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false 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, // Bind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used: // Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding // "application/json" --> JSON binding
@ -318,33 +430,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. // Like ParseBody() but this method also writes a 400 error if the json is not valid.
func (c *Context) Bind(obj interface{}) error { func (c *Context) Bind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType()) b := binding.Default(c.Request.Method, c.ContentType())
return c.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 { 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. // See the binding package.
func (c *Context) BindWith(obj interface{}, b binding.Binding) error { func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
if err := b.Bind(c.Request, obj); err != nil { if err = c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(400, err).SetType(ErrorTypeBind) 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 // ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP { if c.engine.ForwardedByClientIP {
clientIP := strings.TrimSpace(c.requestHeader("X-Real-Ip")) clientIP := c.requestHeader("X-Forwarded-For")
if len(clientIP) > 0 {
return clientIP
}
clientIP = c.requestHeader("X-Forwarded-For")
if index := strings.IndexByte(clientIP, ','); index >= 0 { if index := strings.IndexByte(clientIP, ','); index >= 0 {
clientIP = clientIP[0:index] clientIP = clientIP[0:index]
} }
@ -352,10 +469,22 @@ func (c *Context) ClientIP() string {
if len(clientIP) > 0 { if len(clientIP) > 0 {
return clientIP 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 { if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
return ip return ip
} }
return "" return ""
} }
@ -364,6 +493,16 @@ func (c *Context) ContentType() string {
return filterFlags(c.requestHeader("Content-Type")) 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 { func (c *Context) requestHeader(key string) string {
if values, _ := c.Request.Header[key]; len(values) > 0 { if values, _ := c.Request.Header[key]; len(values) > 0 {
return values[0] return values[0]
@ -375,6 +514,19 @@ func (c *Context) requestHeader(key string) string {
/******** RESPONSE RENDERING ********/ /******** 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) { func (c *Context) Status(code int) {
c.writermem.WriteHeader(code) c.writermem.WriteHeader(code)
} }
@ -390,6 +542,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( func (c *Context) SetCookie(
name string, name string,
value string, value string,
@ -424,6 +586,13 @@ func (c *Context) Cookie(name string) (string, error) {
func (c *Context) Render(code int, r render.Render) { func (c *Context) Render(code int, r render.Render) {
c.Status(code) c.Status(code)
if !bodyAllowedForStatus(code) {
r.WriteContentType(c.Writer)
c.Writer.WriteHeaderNow()
return
}
if err := r.Render(c.Writer); err != nil { if err := r.Render(c.Writer); err != nil {
panic(err) panic(err)
} }
@ -439,7 +608,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. // IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body.
// It also sets the Content-Type as "application/json". // 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. // more CPU and bandwidth consuming. Use Context.JSON() instead.
func (c *Context) IndentedJSON(code int, obj interface{}) { func (c *Context) IndentedJSON(code int, obj interface{}) {
c.Render(code, render.IndentedJSON{Data: obj}) c.Render(code, render.IndentedJSON{Data: obj})
@ -448,10 +617,7 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
// JSON serializes the given struct as JSON into the response body. // JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) { func (c *Context) JSON(code int, obj interface{}) {
c.Status(code) c.Render(code, render.JSON{Data: obj})
if err := render.WriteJSON(c.Writer, obj); err != nil {
panic(err)
}
} }
// XML serializes the given struct as XML into the response body. // XML serializes the given struct as XML into the response body.
@ -467,8 +633,7 @@ func (c *Context) YAML(code int, obj interface{}) {
// String writes the given string into the response body. // String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) { func (c *Context) String(code int, format string, values ...interface{}) {
c.Status(code) c.Render(code, render.String{Format: format, Data: values})
render.WriteString(c.Writer, format, values)
} }
// Redirect returns a HTTP redirect to the specific location. // 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 ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"html/template" "html/template"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/manucorporat/sse" "github.com/gin-contrib/sse"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/context"
) )
var _ context.Context = &Context{}
// Unit tests TODO // Unit tests TODO
// func (c *Context) File(filepath string) { // func (c *Context) File(filepath string) {
// func (c *Context) Negotiate(code int, config Negotiate) { // 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", "first"))
must(mw.WriteField("array", "second")) must(mw.WriteField("array", "second"))
must(mw.WriteField("id", "")) 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) req, err := http.NewRequest("POST", "/", body)
must(err) must(err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) 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) { func TestContextReset(t *testing.T) {
router := New() router := New()
c := router.allocateContext() c := router.allocateContext()
@ -74,7 +112,7 @@ func TestContextReset(t *testing.T) {
} }
func TestContextHandlers(t *testing.T) { func TestContextHandlers(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
assert.Nil(t, c.handlers) assert.Nil(t, c.handlers)
assert.Nil(t, c.handlers.Last()) 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 // TestContextSetGet tests that a parameter is set correctly on the
// current context and can be retrieved using Get. // current context and can be retrieved using Get.
func TestContextSetGet(t *testing.T) { func TestContextSetGet(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("foo", "bar") c.Set("foo", "bar")
value, err := c.Get("foo") value, err := c.Get("foo")
@ -111,7 +149,7 @@ func TestContextSetGet(t *testing.T) {
} }
func TestContextSetGetValues(t *testing.T) { func TestContextSetGetValues(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("string", "this is a string") c.Set("string", "this is a string")
c.Set("int32", int32(-42)) c.Set("int32", int32(-42))
c.Set("int64", int64(42424242424242)) 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) { func TestContextCopy(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.index = 2 c.index = 2
c.Request, _ = http.NewRequest("POST", "/hola", nil) c.Request, _ = http.NewRequest("POST", "/hola", nil)
c.handlers = HandlersChain{func(c *Context) {}} c.handlers = HandlersChain{func(c *Context) {}}
@ -151,7 +268,7 @@ func TestContextCopy(t *testing.T) {
} }
func TestContextHandlerName(t *testing.T) { func TestContextHandlerName(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest} c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName()) 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) { 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) c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil)
value, ok := c.GetQuery("foo") value, ok := c.GetQuery("foo")
@ -197,7 +325,7 @@ func TestContextQuery(t *testing.T) {
} }
func TestContextQueryAndPostForm(t *testing.T) { func TestContextQueryAndPostForm(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second") body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body) c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body)
c.Request.Header.Add("Content-Type", MIMEPOSTForm) c.Request.Header.Add("Content-Type", MIMEPOSTForm)
@ -270,15 +398,18 @@ func TestContextQueryAndPostForm(t *testing.T) {
} }
func TestContextPostFormMultipart(t *testing.T) { func TestContextPostFormMultipart(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request = createMultipartRequest() c.Request = createMultipartRequest()
var obj struct { var obj struct {
Foo string `form:"foo"` Foo string `form:"foo"`
Bar string `form:"bar"` Bar string `form:"bar"`
BarAsInt int `form:"bar"` BarAsInt int `form:"bar"`
Array []string `form:"array"` Array []string `form:"array"`
ID string `form:"id"` 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.NoError(t, c.Bind(&obj))
assert.Equal(t, obj.Foo, "bar") 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.BarAsInt, 10)
assert.Equal(t, obj.Array, []string{"first", "second"}) assert.Equal(t, obj.Array, []string{"first", "second"})
assert.Equal(t, obj.ID, "") 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") value, ok := c.GetQuery("foo")
assert.False(t, ok) assert.False(t, ok)
@ -334,46 +470,115 @@ func TestContextPostFormMultipart(t *testing.T) {
} }
func TestContextSetCookie(t *testing.T) { func TestContextSetCookie(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "/", "localhost", true, true) 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") 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) { func TestContextGetCookie(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request, _ = http.NewRequest("GET", "/get", nil)
c.Request.Header.Set("Cookie", "user=gin") c.Request.Header.Set("Cookie", "user=gin")
cookie, _ := c.Cookie("user") cookie, _ := c.Cookie("user")
assert.Equal(t, cookie, "gin") 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 // Tests that the response is serialized as JSON
// and Content-Type is set to application/json // and Content-Type is set to application/json
func TestContextRenderJSON(t *testing.T) { func TestContextRenderJSON(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.JSON(201, H{"foo": "bar"}) c.JSON(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") 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 // Tests that the response is serialized as JSON
// we change the content-type before // we change the content-type before
func TestContextRenderAPIJSON(t *testing.T) { func TestContextRenderAPIJSON(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "application/vnd.api+json") c.Header("Content-Type", "application/vnd.api+json")
c.JSON(201, H{"foo": "bar"}) c.JSON(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") 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") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json")
} }
// Tests that the response is serialized as JSON // Tests that the response is serialized as JSON
// and Content-Type is set to application/json // and Content-Type is set to application/json
func TestContextRenderIndentedJSON(t *testing.T) { 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"}}) c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
assert.Equal(t, w.Code, 201) 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") 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 // Tests that the response executes the templates
// and responds with Content-Type set to text/html // and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) { 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}}`)) templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ) 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") 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 // TestContextXML tests that the response is serialized as XML
// and Content-Type is set to application/xml // and Content-Type is set to application/xml
func TestContextRenderXML(t *testing.T) { func TestContextRenderXML(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.XML(201, H{"foo": "bar"}) c.XML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) 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") 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 // TestContextString tests that the response is returned
// with Content-Type set to text/plain // with Content-Type set to text/plain
func TestContextRenderString(t *testing.T) { func TestContextRenderString(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.String(201, "test %s %d", "string", 2) c.String(201, "test %s %d", "string", 2)
assert.Equal(t, w.Code, 201) 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") 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 // TestContextString tests that the response is returned
// with Content-Type set to text/html // with Content-Type set to text/html
func TestContextRenderHTMLString(t *testing.T) { func TestContextRenderHTMLString(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8") c.Header("Content-Type", "text/html; charset=utf-8")
c.String(201, "<html>%s %d</html>", "string", 3) 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") 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` // TestContextData tests that the response can be written from `bytesting`
// with specified MIME type // with specified MIME type
func TestContextRenderData(t *testing.T) { func TestContextRenderData(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Data(201, "text/csv", []byte(`foo,bar`)) c.Data(201, "text/csv", []byte(`foo,bar`))
assert.Equal(t, w.Code, 201) 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") 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) { func TestContextRenderSSE(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.SSEvent("float", 1.5) c.SSEvent("float", 1.5)
c.Render(-1, sse.Event{ c.Render(-1, sse.Event{
Id: "123", Id: "123",
@ -456,7 +747,9 @@ func TestContextRenderSSE(t *testing.T) {
} }
func TestContextRenderFile(t *testing.T) { func TestContextRenderFile(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil) c.Request, _ = http.NewRequest("GET", "/", nil)
c.File("./gin.go") c.File("./gin.go")
@ -468,7 +761,9 @@ func TestContextRenderFile(t *testing.T) {
// TestContextRenderYAML tests that the response is serialized as YAML // TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/x-yaml // and Content-Type is set to application/x-yaml
func TestContextRenderYAML(t *testing.T) { func TestContextRenderYAML(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.YAML(201, H{"foo": "bar"}) c.YAML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, w.Code, 201)
@ -477,7 +772,7 @@ func TestContextRenderYAML(t *testing.T) {
} }
func TestContextHeaders(t *testing.T) { func TestContextHeaders(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Header("Content-Type", "text/plain") c.Header("Content-Type", "text/plain")
c.Header("X-Custom", "value") c.Header("X-Custom", "value")
@ -494,7 +789,9 @@ func TestContextHeaders(t *testing.T) {
// TODO // TODO
func TestContextRenderRedirectWithRelativePath(t *testing.T) { func TestContextRenderRedirectWithRelativePath(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
assert.Panics(t, func() { c.Redirect(299, "/new_path") }) assert.Panics(t, func() { c.Redirect(299, "/new_path") })
assert.Panics(t, func() { c.Redirect(309, "/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) { func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Redirect(302, "http://google.com") c.Redirect(302, "http://google.com")
c.Writer.WriteHeaderNow() c.Writer.WriteHeaderNow()
@ -516,7 +815,9 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
} }
func TestContextRenderRedirectWith201(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.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Redirect(201, "/resource") c.Redirect(201, "/resource")
c.Writer.WriteHeaderNow() c.Writer.WriteHeaderNow()
@ -526,7 +827,7 @@ func TestContextRenderRedirectWith201(t *testing.T) {
} }
func TestContextRenderRedirectAll(t *testing.T) { func TestContextRenderRedirectAll(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
assert.Panics(t, func() { c.Redirect(200, "/resource") }) assert.Panics(t, func() { c.Redirect(200, "/resource") })
assert.Panics(t, func() { c.Redirect(202, "/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") }) 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) { func TestContextNegotiationFormat(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "", nil) c.Request, _ = http.NewRequest("POST", "", nil)
assert.Panics(t, func() { c.NegotiateFormat() }) assert.Panics(t, func() { c.NegotiateFormat() })
@ -546,7 +909,7 @@ func TestContextNegotiationFormat(t *testing.T) {
} }
func TestContextNegotiationFormatWithAccept(t *testing.T) { func TestContextNegotiationFormatWithAccept(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") 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) { func TestContextNegotiationFormatCustum(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") 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) { func TestContextIsAborted(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
assert.False(t, c.IsAborted()) assert.False(t, c.IsAborted())
c.Abort() c.Abort()
@ -585,7 +948,9 @@ func TestContextIsAborted(t *testing.T) {
// TestContextData tests that the response can be written from `bytesting` // TestContextData tests that the response can be written from `bytesting`
// with specified MIME type // with specified MIME type
func TestContextAbortWithStatus(t *testing.T) { func TestContextAbortWithStatus(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.index = 4 c.index = 4
c.AbortWithStatus(401) c.AbortWithStatus(401)
@ -595,8 +960,38 @@ func TestContextAbortWithStatus(t *testing.T) {
assert.True(t, c.IsAborted()) 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) { func TestContextError(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
assert.Empty(t, c.Errors) assert.Empty(t, c.Errors)
c.Error(errors.New("first error")) c.Error(errors.New("first error"))
@ -622,7 +1017,7 @@ func TestContextError(t *testing.T) {
} }
func TestContextTypedError(t *testing.T) { func TestContextTypedError(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
@ -636,7 +1031,9 @@ func TestContextTypedError(t *testing.T) {
} }
func TestContextAbortWithError(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") c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
assert.Equal(t, w.Code, 401) assert.Equal(t, w.Code, 401)
@ -645,27 +1042,37 @@ func TestContextAbortWithError(t *testing.T) {
} }
func TestContextClientIP(t *testing.T) { func TestContextClientIP(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") 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-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 " c.Request.RemoteAddr = " 40.40.40.40:42123 "
assert.Equal(t, c.ClientIP(), "10.10.10.10") assert.Equal(t, "20.20.20.20", c.ClientIP())
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")
c.Request.Header.Del("X-Forwarded-For") 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) { func TestContextContentType(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
@ -673,7 +1080,7 @@ func TestContextContentType(t *testing.T) {
} }
func TestContextAutoBindJSON(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, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON) c.Request.Header.Add("Content-Type", MIMEJSON)
@ -688,7 +1095,9 @@ func TestContextAutoBindJSON(t *testing.T) {
} }
func TestContextBindWithJSON(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, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
@ -703,7 +1112,9 @@ func TestContextBindWithJSON(t *testing.T) {
} }
func TestContextBadAutoBind(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, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON) c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct { var obj struct {
@ -722,7 +1133,7 @@ func TestContextBadAutoBind(t *testing.T) {
} }
func TestContextGolangContext(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\"}")) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
assert.NoError(t, c.Err()) assert.NoError(t, c.Err())
assert.Nil(t, c.Done()) assert.Nil(t, c.Done())
@ -736,3 +1147,45 @@ func TestContextGolangContext(t *testing.T) {
assert.Equal(t, c.Value("foo"), "bar") assert.Equal(t, c.Value("foo"), "bar")
assert.Nil(t, c.Value(1)) 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 ( import (
"bytes" "bytes"
"errors" "errors"
"html/template"
"io" "io"
"log" "log"
"os" "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()) 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.Equal(t, w.String(), "[GIN-debug] Loaded HTML Templates (2): \n\t- \n\t- hello.tmpl\n\n")
}
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) { func setup(w io.Writer) {
SetMode(DebugMode) SetMode(DebugMode)
log.SetOutput(w) log.SetOutput(w)

View File

@ -4,9 +4,22 @@
package gin package gin
import "log" import (
"github.com/gin-gonic/gin/binding"
"log"
)
func (c *Context) GetCookie(name string) (string, error) { func (c *Context) GetCookie(name string) (string, error) {
log.Println("GetCookie() method is deprecated. Use Cookie() instead.") log.Println("GetCookie() method is deprecated. Use Cookie() instead.")
return c.Cookie(name) return c.Cookie(name)
} }
// 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)
}

View File

@ -72,7 +72,7 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
} }
// Implements the error interface // Implements the error interface
func (msg *Error) Error() string { func (msg Error) Error() string {
return msg.Err.Error() return msg.Err.Error()
} }
@ -80,7 +80,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0 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 // ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic
func (a errorMsgs) ByType(typ ErrorType) errorMsgs { func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
if len(a) == 0 { if len(a) == 0 {

View File

@ -54,6 +54,13 @@ func TestError(t *testing.T) {
"status": "200", "status": "200",
"data": "some data", "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) { func TestErrorSlice(t *testing.T) {

View File

@ -1,8 +1,9 @@
package hello package hello
import ( import (
"github.com/gin-gonic/gin"
"net/http" "net/http"
"github.com/gin-gonic/gin"
) )
// This function's name is a must. App Engine uses it to drive the requests properly. // 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) var DB = make(map[string]string)
func main() { func main() {
// Disable Console Color
// gin.DisableConsoleColor()
r := gin.Default() r := gin.Default()
// Ping test // 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) { func rateLimit(c *gin.Context) {
ip := c.ClientIP() ip := c.ClientIP()
value := int(ips.Add(ip, 1)) value := int(ips.Add(ip, 1))
if value%50 == 0 { if value%50 == 0 {

View File

@ -8,11 +8,13 @@ import (
"github.com/manucorporat/stats" "github.com/manucorporat/stats"
) )
var ips = stats.New() var (
var messages = stats.New() ips = stats.New()
var users = stats.New() messages = stats.New()
var mutexStats sync.RWMutex users = stats.New()
var savedStats map[string]uint64 mutexStats sync.RWMutex
savedStats map[string]uint64
)
func statsWorker() { func statsWorker() {
c := time.Tick(1 * time.Second) 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,39 @@
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, _ := c.MultipartForm()
files := form.File["files"]
for _, file := range files {
// Source
src, _ := file.Open()
defer src.Close()
// Destination
dst, _ := os.Create(file.Filename)
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,34 @@
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, _ := c.FormFile("file")
src, _ := file.Open()
defer src.Close()
// Destination
dst, _ := os.Create(file.Filename)
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}]}

6
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 package gin
import ( import (
@ -14,7 +18,7 @@ type (
} }
) )
// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used interally // Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally
// in router.Static(). // in router.Static().
// if listDirectory == true, then it works the same as http.Dir() otherwise it returns // 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. // a filesystem that prevents http.FileServer() to list the directory files.

72
gin.go
View File

@ -15,10 +15,11 @@ import (
) )
// Version is Framework's version // Version is Framework's version
const Version = "v1.0rc2" const Version = "v1.2"
var default404Body = []byte("404 page not found") var default404Body = []byte("404 page not found")
var default405Body = []byte("405 method not allowed") var default405Body = []byte("405 method not allowed")
var defaultAppEngine bool
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
@ -44,7 +45,9 @@ type (
// Create an instance of Engine, by using New() or Default() // Create an instance of Engine, by using New() or Default()
Engine struct { Engine struct {
RouterGroup RouterGroup
delims render.Delims
HTMLRender render.HTMLRender HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain allNoRoute HandlersChain
allNoMethod HandlersChain allNoMethod HandlersChain
noRoute HandlersChain noRoute HandlersChain
@ -78,6 +81,17 @@ type (
// handler. // handler.
HandleMethodNotAllowed bool HandleMethodNotAllowed bool
ForwardedByClientIP 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
} }
) )
@ -89,6 +103,8 @@ var _ IRouter = &Engine{}
// - RedirectFixedPath: false // - RedirectFixedPath: false
// - HandleMethodNotAllowed: false // - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true // - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New() *Engine { func New() *Engine {
debugPrintWARNINGNew() debugPrintWARNINGNew()
engine := &Engine{ engine := &Engine{
@ -97,11 +113,16 @@ func New() *Engine {
basePath: "/", basePath: "/",
root: true, root: true,
}, },
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true, RedirectTrailingSlash: true,
RedirectFixedPath: false, RedirectFixedPath: false,
HandleMethodNotAllowed: false, HandleMethodNotAllowed: false,
ForwardedByClientIP: true, ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
UseRawPath: false,
UnescapePathValues: true,
trees: make(methodTrees, 0, 9), trees: make(methodTrees, 0, 9),
delims: render.Delims{"{{", "}}"},
} }
engine.RouterGroup.engine = engine engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} { engine.pool.New = func() interface{} {
@ -121,21 +142,26 @@ func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine} 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) { func (engine *Engine) LoadHTMLGlob(pattern string) {
if IsDebugging() { if IsDebugging() {
debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern))) debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern)))
engine.HTMLRender = render.HTMLDebug{Glob: pattern} engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
} else { } 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) engine.SetHTMLTemplate(templ)
} }
} }
func (engine *Engine) LoadHTMLFiles(files ...string) { func (engine *Engine) LoadHTMLFiles(files ...string) {
if IsDebugging() { if IsDebugging() {
engine.HTMLRender = render.HTMLDebug{Files: files} engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims}
} else { } 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) engine.SetHTMLTemplate(templ)
} }
} }
@ -144,7 +170,12 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 { if len(engine.trees) > 0 {
debugPrintWARNINGSetHTMLTemplate() 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. // NoRoute adds handlers for NoRoute. It return a 404 code by default.
@ -267,9 +298,26 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.pool.Put(c) 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) { func (engine *Engine) handleHTTPRequest(context *Context) {
httpMethod := context.Request.Method 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 // Find root of the tree for the given HTTP method
t := engine.trees t := engine.trees
@ -277,15 +325,15 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
if t[i].method == httpMethod { if t[i].method == httpMethod {
root := t[i].root root := t[i].root
// Find route in tree // Find route in tree
handlers, params, tsr := root.getValue(path, context.Params) handlers, params, tsr := root.getValue(path, context.Params, unescape)
if handlers != nil { if handlers != nil {
context.handlers = handlers context.handlers = handlers
context.Params = params context.Params = params
context.Next() context.Next()
context.writermem.WriteHeaderNow() context.writermem.WriteHeaderNow()
return return
}
} else if httpMethod != "CONNECT" && path != "/" { if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectTrailingSlash { if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(context) redirectTrailingSlash(context)
return return
@ -302,7 +350,7 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
if engine.HandleMethodNotAllowed { if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees { for _, tree := range engine.trees {
if tree.method != httpMethod { if tree.method != httpMethod {
if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil { if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
context.handlers = engine.allNoMethod context.handlers = engine.allNoMethod
serveError(context, 405, default405Body) serveError(context, 405, default405Body)
return return

View File

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

View File

@ -5,14 +5,98 @@
package gin package gin
import ( import (
"fmt"
"html/template"
"io/ioutil"
"net/http"
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert" "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 //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) LoadHTMLFiles(files ...string) {
// func (engine *Engine) RunTLS(addr string, cert string, key string) error { // func (engine *Engine) RunTLS(addr string, cert string, key string) error {
@ -42,6 +126,32 @@ func TestCreateEngine(t *testing.T) {
// SetMode(TestMode) // 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) { 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,16 +14,21 @@ import (
) )
var ( var (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109}) reset = string([]byte{27, 91, 48, 109})
disableColor = false
) )
func DisableConsoleColor() {
disableColor = true
}
func ErrorLogger() HandlerFunc { func ErrorLogger() HandlerFunc {
return ErrorLoggerT(ErrorTypeAny) return ErrorLoggerT(ErrorTypeAny)
} }
@ -49,7 +54,9 @@ func Logger() HandlerFunc {
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
isTerm := true 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 isTerm = false
} }
@ -87,12 +94,12 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
} }
comment := c.Errors.ByType(ErrorTypePrivate).String() 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"), end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, reset, statusColor, statusCode, reset,
latency, latency,
clientIP, clientIP,
methodColor, reset, method, methodColor, method, reset,
path, path,
comment, 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 // I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather // like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go. // than individual functions. Im not sure where these should go.
buffer.Reset()
performRequest(router, "POST", "/example") performRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST") assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PUT", "/example") performRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT") assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "DELETE", "/example") performRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE") assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "PATCH", "/example") performRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH") assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "HEAD", "/example") performRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD") assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "OPTIONS", "/example") performRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS") assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example") assert.Contains(t, buffer.String(), "/example")
buffer.Reset()
performRequest(router, "GET", "/notfound") performRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404") assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET") assert.Contains(t, buffer.String(), "GET")
@ -107,16 +113,16 @@ func TestErrorLogger(t *testing.T) {
}) })
w := performRequest(router, "GET", "/error") w := performRequest(router, "GET", "/error")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
assert.Equal(t, w.Body.String(), "{\"error\":\"this is an error\"}\n") assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort") w = performRequest(router, "GET", "/abort")
assert.Equal(t, w.Code, 401) assert.Equal(t, 401, w.Code)
assert.Equal(t, w.Body.String(), "{\"error\":\"no authorized\"}\n") assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print") w = performRequest(router, "GET", "/print")
assert.Equal(t, w.Code, 500) assert.Equal(t, 500, w.Code)
assert.Equal(t, w.Body.String(), "hola!{\"error\":\"this is an error\"}\n") assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
} }
func TestSkippingPaths(t *testing.T) { func TestSkippingPaths(t *testing.T) {
@ -129,6 +135,14 @@ func TestSkippingPaths(t *testing.T) {
performRequest(router, "GET", "/logged") performRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200") assert.Contains(t, buffer.String(), "200")
buffer.Reset()
performRequest(router, "GET", "/skipped") performRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }
func TestDisableConsoleColor(t *testing.T) {
New()
assert.False(t, disableColor)
DisableConsoleColor()
assert.True(t, disableColor)
}

View File

@ -7,10 +7,9 @@ package gin
import ( import (
"errors" "errors"
"strings" "strings"
"testing" "testing"
"github.com/manucorporat/sse" "github.com/gin-contrib/sse"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -245,6 +244,6 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 400) assert.Equal(t, 400, w.Code)
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, 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" TestMode string = "test"
) )
const ( const (
debugCode = iota debugCode = iota
releaseCode releaseCode
testCode testCode
) )
// DefaultWriter is the default io.Writer used the Gin for debug output and // DefaultWriter is the default io.Writer used the Gin for debug output and

View File

@ -1,7 +1,7 @@
// Copyright 2013 Julien Schmidt. All rights reserved. // Copyright 2013 Julien Schmidt. All rights reserved.
// Based on the path package, Copyright 2009 The Go Authors. // 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 // 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 package gin

View File

@ -1,7 +1,7 @@
// Copyright 2013 Julien Schmidt. All rights reserved. // Copyright 2013 Julien Schmidt. All rights reserved.
// Based on the path package, Copyright 2009 The Go Authors. // 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 // 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 package gin

View File

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

View File

@ -10,17 +10,25 @@ import (
) )
type ( type (
Delims struct {
Left string
Right string
}
HTMLRender interface { HTMLRender interface {
Instance(string, interface{}) Render Instance(string, interface{}) Render
} }
HTMLProduction struct { HTMLProduction struct {
Template *template.Template Template *template.Template
Delims Delims
} }
HTMLDebug struct { HTMLDebug struct {
Files []string Files []string
Glob string Glob string
Delims Delims
FuncMap template.FuncMap
} }
HTML struct { HTML struct {
@ -48,19 +56,27 @@ func (r HTMLDebug) Instance(name string, data interface{}) Render {
} }
} }
func (r HTMLDebug) loadTemplate() *template.Template { func (r HTMLDebug) loadTemplate() *template.Template {
if r.FuncMap == nil {
r.FuncMap = template.FuncMap{}
}
if len(r.Files) > 0 { 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 { 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") panic("the HTML debug render was created without files or glob pattern")
} }
func (r HTML) Render(w http.ResponseWriter) error { func (r HTML) Render(w http.ResponseWriter) error {
writeContentType(w, htmlContentType) r.WriteContentType(w)
if len(r.Name) == 0 { if len(r.Name) == 0 {
return r.Template.Execute(w, r.Data) return r.Template.Execute(w, r.Data)
} }
return r.Template.ExecuteTemplate(w, r.Name, 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"} var jsonContentType = []string{"application/json; charset=utf-8"}
func (r JSON) Render(w http.ResponseWriter) error { func (r JSON) Render(w http.ResponseWriter) (err error) {
return WriteJSON(w, r.Data) 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 { func (r IndentedJSON) Render(w http.ResponseWriter) error {
writeContentType(w, jsonContentType) r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ") jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
if err != nil { if err != nil {
return err return err
@ -35,7 +52,6 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error {
return nil return nil
} }
func WriteJSON(w http.ResponseWriter, obj interface{}) error { func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) 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) http.Redirect(w, r.Request, r.Location, r.Code)
return nil return nil
} }
func (r Redirect) WriteContentType(http.ResponseWriter) {}

View File

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

View File

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

View File

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

View File

@ -16,6 +16,10 @@ type XML struct {
var xmlContentType = []string{"application/xml; charset=utf-8"} var xmlContentType = []string{"application/xml; charset=utf-8"}
func (r XML) Render(w http.ResponseWriter) error { func (r XML) Render(w http.ResponseWriter) error {
writeContentType(w, xmlContentType) r.WriteContentType(w)
return xml.NewEncoder(w).Encode(r.Data) 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"} var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
func (r YAML) Render(w http.ResponseWriter) error { func (r YAML) Render(w http.ResponseWriter) error {
writeContentType(w, yamlContentType) r.WriteContentType(w)
bytes, err := yaml.Marshal(r.Data) bytes, err := yaml.Marshal(r.Data)
if err != nil { if err != nil {
@ -27,3 +27,7 @@ func (r YAML) Render(w http.ResponseWriter) error {
w.Write(bytes) w.Write(bytes)
return nil return nil
} }
func (r YAML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, yamlContentType)
}

View File

@ -400,3 +400,42 @@ func TestRouterNotFound(t *testing.T) {
w = performRequest(router, "GET", "/") w = performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 404) 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)
}

17
test_helpers.go Normal file
View File

@ -0,0 +1,17 @@
// 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"
)
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
r = New()
c = r.allocateContext()
c.reset()
c.writermem.reset(w)
return
}

27
tree.go
View File

@ -1,10 +1,11 @@
// Copyright 2013 Julien Schmidt. All rights reserved. // Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found // 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 package gin
import ( import (
"net/url"
"strings" "strings"
"unicode" "unicode"
) )
@ -363,7 +364,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 // 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 // made if a handle exists with an extra (without the) trailing slash for the
// given path. // 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 p = po
walk: // Outer loop for walking the tree walk: // Outer loop for walking the tree
for { for {
@ -406,7 +407,15 @@ walk: // Outer loop for walking the tree
i := len(p) i := len(p)
p = p[:i+1] // expand slice within preallocated capacity p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[1:] 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! // we need to go deeper!
if end < len(path) { if end < len(path) {
@ -423,7 +432,8 @@ walk: // Outer loop for walking the tree
if handlers = n.handlers; handlers != nil { if handlers = n.handlers; handlers != nil {
return return
} else if len(n.children) == 1 { }
if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a // No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation // trailing slash exists for TSR recommendation
n = n.children[0] n = n.children[0]
@ -440,7 +450,14 @@ walk: // Outer loop for walking the tree
i := len(p) i := len(p)
p = p[:i+1] // expand slice within preallocated capacity p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[2:] 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 handlers = n.handlers
return return

View File

@ -1,6 +1,6 @@
// Copyright 2013 Julien Schmidt. All rights reserved. // Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found // 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 package gin
@ -37,9 +37,14 @@ type testRequests []struct {
ps Params 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 { for _, request := range requests {
handler, ps, _ := tree.getValue(request.path, nil) handler, ps, _ := tree.getValue(request.path, nil, unescape)
if handler == nil { if handler == nil {
if !request.nilHandler { if !request.nilHandler {
@ -197,6 +202,45 @@ func TestTreeWildcard(t *testing.T) {
checkMaxParams(t, tree) 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{}) { func catchPanic(testFunc func()) (recv interface{}) {
defer func() { defer func() {
recv = recover() recv = recover()
@ -430,7 +474,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/doc/", "/doc/",
} }
for _, route := range tsrRoutes { for _, route := range tsrRoutes {
handler, _, tsr := tree.getValue(route, nil) handler, _, tsr := tree.getValue(route, nil, false)
if handler != nil { if handler != nil {
t.Fatalf("non-nil handler for TSR route '%s", route) t.Fatalf("non-nil handler for TSR route '%s", route)
} else if !tsr { } else if !tsr {
@ -447,7 +491,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/api/world/abc", "/api/world/abc",
} }
for _, route := range noTsrRoutes { for _, route := range noTsrRoutes {
handler, _, tsr := tree.getValue(route, nil) handler, _, tsr := tree.getValue(route, nil, false)
if handler != nil { if handler != nil {
t.Fatalf("non-nil handler for No-TSR route '%s", route) t.Fatalf("non-nil handler for No-TSR route '%s", route)
} else if tsr { } else if tsr {
@ -466,7 +510,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
t.Fatalf("panic inserting test route: %v", recv) t.Fatalf("panic inserting test route: %v", recv)
} }
handler, _, tsr := tree.getValue("/", nil) handler, _, tsr := tree.getValue("/", nil, false)
if handler != nil { if handler != nil {
t.Fatalf("non-nil handler") t.Fatalf("non-nil handler")
} else if tsr { } else if tsr {
@ -617,7 +661,7 @@ func TestTreeInvalidNodeType(t *testing.T) {
// normal lookup // normal lookup
recv := catchPanic(func() { recv := catchPanic(func() {
tree.getValue("/test", nil) tree.getValue("/test", nil, false)
}) })
if rs, ok := recv.(string); !ok || rs != panicMsg { if rs, ok := recv.(string); !ok || rs != panicMsg {
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) 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"
}