diff --git a/.gitignore b/.gitignore
index 9f48f142..f3b636df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-Godeps/*
-!Godeps/Godeps.json
+vendor/*
+!vendor/vendor.json
coverage.out
count.out
diff --git a/.travis.yml b/.travis.yml
index 53f436f4..6532a334 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,23 @@
language: go
sudo: false
go:
- - 1.4
- - 1.5.4
- - 1.6.4
- - 1.7.4
- - tip
+ - 1.6.x
+ - 1.7.x
+ - 1.8.x
+ - master
+
+git:
+ depth: 3
+
+install:
+ - make install
script:
- - go test -v -covermode=count -coverprofile=coverage.out
+ - make vet
+ - make fmt-check
+ - make embedmd
+ - make misspell-check
+ - make test
after_success:
- bash <(curl -s https://codecov.io/bash)
diff --git a/AUTHORS.md b/AUTHORS.md
index 2feaf467..7ab7213d 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -1,8 +1,6 @@
List of all the awesome people working to make Gin the best Web Framework in Go.
-
-
-##gin 0.x series authors
+## gin 0.x series authors
**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
@@ -226,4 +224,4 @@ People and companies, who have contributed, in alphabetical order.
**@yuyabee**
-- Fixed README
\ No newline at end of file
+- Fixed README
diff --git a/BENCHMARKS.md b/BENCHMARKS.md
index 181f75b3..6efe3ca4 100644
--- a/BENCHMARKS.md
+++ b/BENCHMARKS.md
@@ -295,4 +295,4 @@ BenchmarkPossum_GPlusAll 100000 19685 ns/op 6240 B/op
BenchmarkR2router_GPlusAll 100000 16251 ns/op 5040 B/op 76 allocs/op
BenchmarkRevel_GPlusAll 20000 93489 ns/op 21656 B/op 368 allocs/op
BenchmarkRivet_GPlusAll 100000 16907 ns/op 5408 B/op 64 allocs/op
-```
\ No newline at end of file
+```
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82f1bead..ee485ec3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,47 @@
-#CHANGELOG
+# CHANGELOG
-###Gin 1.0rc2 (...)
+### Gin 1.2
+
+- [NEW] Switch from godeps to govendor
+- [NEW] Add support for Let's Encrypt via gin-gonic/autotls
+- [NEW] Improve README examples and add extra at examples folder
+- [NEW] Improved support with App Engine
+- [NEW] Add custom template delimiters, see #860
+- [NEW] Add Template Func Maps, see #962
+- [NEW] Add \*context.Handler(), see #928
+- [NEW] Add \*context.GetRawData()
+- [NEW] Add \*context.GetHeader() (request)
+- [NEW] Add \*context.AbortWithStatusJSON() (JSON content type)
+- [NEW] Add \*context.Keys type cast helpers
+- [NEW] Add \*context.ShouldBindWith()
+- [NEW] Add \*context.MustBindWith()
+- [NEW] Add \*engine.SetFuncMap()
+- [DEPRECATE] On next release: \*context.BindWith(), see #855
+- [FIX] Refactor render
+- [FIX] Reworked tests
+- [FIX] logger now supports cygwin
+- [FIX] Use X-Forwarded-For before X-Real-Ip
+- [FIX] time.Time binding (#904)
+
+### Gin 1.1.4
+
+- [NEW] Support google appengine for IsTerminal func
+
+### Gin 1.1.3
+
+- [FIX] Reverted Logger: skip ANSI color commands
+
+### Gin 1.1
+
+- [NEW] Implement QueryArray and PostArray methods
+- [NEW] Refactor GetQuery and GetPostForm
+- [NEW] Add contribution guide
+- [FIX] Corrected typos in README
+- [FIX] Removed additional Iota
+- [FIX] Changed imports to gopkg instead of github in README (#733)
+- [FIX] Logger: skip ANSI color commands if output is not a tty
+
+### Gin 1.0rc2 (...)
- [PERFORMANCE] Fast path for writing Content-Type.
- [PERFORMANCE] Much faster 404 routing
@@ -35,7 +76,7 @@
- [FIX] MIT license in every file
-###Gin 1.0rc1 (May 22, 2015)
+### Gin 1.0rc1 (May 22, 2015)
- [PERFORMANCE] Zero allocation router
- [PERFORMANCE] Faster JSON, XML and text rendering
@@ -79,7 +120,7 @@
- [FIX] Better support for Google App Engine (using log instead of fmt)
-###Gin 0.6 (Mar 9, 2015)
+### Gin 0.6 (Mar 9, 2015)
- [NEW] Support multipart/form-data
- [NEW] NoMethod handler
@@ -89,14 +130,14 @@
- [FIX] Improve color logger
-###Gin 0.5 (Feb 7, 2015)
+### Gin 0.5 (Feb 7, 2015)
- [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding
-###Gin 0.4 (Aug 21, 2014)
+### Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode
- [NEW] Unit tests
@@ -105,7 +146,7 @@
- [FIX] Improved documentation for model binding
-###Gin 0.3 (Jul 18, 2014)
+### Gin 0.3 (Jul 18, 2014)
- [PERFORMANCE] Normal log and error log are printed in the same call.
- [PERFORMANCE] Improve performance of NoRouter()
@@ -123,7 +164,7 @@
- [FIX] Check application/x-www-form-urlencoded when parsing form
-###Gin 0.2b (Jul 08, 2014)
+### Gin 0.2b (Jul 08, 2014)
- [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
- [NEW] Travis CI integration
- [NEW] Completely new logger
diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json
deleted file mode 100644
index a9c828a2..00000000
--- a/Godeps/Godeps.json
+++ /dev/null
@@ -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"
- }
- ]
-}
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..9ba475a4
--- /dev/null
+++ b/Makefile
@@ -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)
diff --git a/README.md b/README.md
index dc9c9e33..029606b7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-
# Gin Web Framework
@@ -16,10 +15,11 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
```sh
$ cat test.go
```
+
```go
package main
-import "gopkg.in/gin-gonic/gin.v1"
+import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
@@ -87,28 +87,31 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648
1. Download and install it:
- ```sh
- $ go get gopkg.in/gin-gonic/gin.v1
- ```
+```sh
+$ go get github.com/gin-gonic/gin
+```
2. Import it in your code:
- ```go
- import "gopkg.in/gin-gonic/gin.v1"
- ```
+```go
+import "github.com/gin-gonic/gin"
+```
3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
- ```go
- import "net/http"
- ```
+```go
+import "net/http"
+```
## API Examples
-#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
+### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
```go
func main() {
+ // Disable Console Color
+ // gin.DisableConsoleColor()
+
// Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware
router := gin.Default()
@@ -128,7 +131,7 @@ func main() {
}
```
-#### Parameters in path
+### Parameters in path
```go
func main() {
@@ -153,7 +156,8 @@ func main() {
}
```
-#### Querystring parameters
+### Querystring parameters
+
```go
func main() {
router := gin.Default()
@@ -220,34 +224,66 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great
```
-### Another example: upload file
+### Upload files
-References issue [#548](https://github.com/gin-gonic/gin/issues/548).
+#### Single file
+
+References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
```go
func main() {
router := gin.Default()
-
router.POST("/upload", func(c *gin.Context) {
+ // single file
+ file, _ := c.FormFile("file")
+ log.Println(file.Filename)
- file, header , err := c.Request.FormFile("upload")
- filename := header.Filename
- fmt.Println(header.Filename)
- out, err := os.Create("./tmp/"+filename+".png")
- if err != nil {
- log.Fatal(err)
- }
- defer out.Close()
- _, err = io.Copy(out, file)
- if err != nil {
- log.Fatal(err)
- }
+ c.String(http.StatusOK, fmt.Printf("'%s' uploaded!", file.Filename))
})
router.Run(":8080")
}
```
-#### Grouping routes
+How to `curl`:
+
+```bash
+curl -X POST http://localhost:8080/upload \
+ -F "file=@/Users/appleboy/test.zip" \
+ -H "Content-Type: multipart/form-data"
+```
+
+#### Multiple files
+
+See the detail [example code](examples/upload-file/multiple).
+
+```go
+func main() {
+ router := gin.Default()
+ router.POST("/upload", func(c *gin.Context) {
+ // Multipart form
+ form, _ := c.MultipartForm()
+ files := form.File["upload[]"]
+
+ for _, file := range files {
+ log.Println(file.Filename)
+ }
+ c.String(http.StatusOK, fmt.Printf("%d files uploaded!", len(files)))
+ })
+ router.Run(":8080")
+}
+```
+
+How to `curl`:
+
+```bash
+curl -X POST http://localhost:8080/upload \
+ -F "upload[]=@/Users/appleboy/test1.zip" \
+ -F "upload[]=@/Users/appleboy/test2.zip" \
+ -H "Content-Type: multipart/form-data"
+```
+
+### Grouping routes
+
```go
func main() {
router := gin.Default()
@@ -272,14 +308,14 @@ func main() {
}
```
-
-#### Blank Gin without middleware by default
+### Blank Gin without middleware by default
Use
```go
r := gin.New()
```
+
instead of
```go
@@ -287,7 +323,7 @@ r := gin.Default()
```
-#### Using middleware
+### Using middleware
```go
func main() {
// 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).
@@ -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
package main
import (
- "gopkg.in/gin-gonic/gin.v1"
+ "github.com/gin-gonic/gin"
)
type LoginForm struct {
@@ -390,7 +461,7 @@ func main() {
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
// you can bind multipart form with explicit binding declaration:
- // c.BindWith(&form, binding.Form)
+ // c.MustBindWith(&form, binding.Form)
// or you can simply use autobinding with Bind method:
var form LoginForm
// in this case proper binding will be automatically selected
@@ -411,8 +482,7 @@ Test it with:
$ curl -v --form user=user --form password=password http://localhost:8080/login
```
-
-#### XML, JSON and YAML rendering
+### XML, JSON and YAML rendering
```go
func main() {
@@ -451,7 +521,7 @@ func main() {
}
```
-####Serving static files
+### Serving static files
```go
func main() {
@@ -465,9 +535,9 @@ func main() {
}
```
-####HTML rendering
+### HTML rendering
-Using LoadHTMLTemplates()
+Using LoadHTMLGlob() or LoadHTMLFiles()
```go
func main() {
@@ -482,7 +552,9 @@ func main() {
router.Run(":8080")
}
```
+
templates/index.tmpl
+
```html
@@ -510,7 +582,9 @@ func main() {
router.Run(":8080")
}
```
+
templates/posts/index.tmpl
+
```html
{{ define "posts/index.tmpl" }}
@@ -520,7 +594,9 @@ templates/posts/index.tmpl
{{ end }}
```
+
templates/users/index.tmpl
+
```html
{{ define "users/index.tmpl" }}
@@ -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:
@@ -557,7 +684,7 @@ r.GET("/test", func(c *gin.Context) {
Both internal and external locations are supported.
-#### Custom Middleware
+### Custom Middleware
```go
func Logger() gin.HandlerFunc {
@@ -597,7 +724,8 @@ func main() {
}
```
-#### Using BasicAuth() middleware
+### Using BasicAuth() middleware
+
```go
// simulate some private data
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.
```go
@@ -668,7 +796,7 @@ func main() {
}
```
-#### Custom HTTP configuration
+### Custom HTTP configuration
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?
There are some ways this can be done.
@@ -712,6 +899,62 @@ endless.ListenAndServe(":4242", router)
An alternative to endless:
* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully.
+* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server.
+* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers.
+
+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
@@ -726,7 +969,7 @@ An alternative to endless:
- 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.
-## Example
+## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
diff --git a/benchmarks_test.go b/benchmarks_test.go
index ebe9804c..a2c62ba3 100644
--- a/benchmarks_test.go
+++ b/benchmarks_test.go
@@ -1,3 +1,7 @@
+// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
package gin
import (
diff --git a/binding/binding.go b/binding/binding.go
index dc7397f1..1dbf2460 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -15,6 +15,8 @@ const (
MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
+ MIMEMSGPACK = "application/x-msgpack"
+ MIMEMSGPACK2 = "application/msgpack"
)
type Binding interface {
@@ -40,22 +42,25 @@ var (
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
+ MsgPack = msgpackBinding{}
)
func Default(method, contentType string) Binding {
if method == "GET" {
return Form
- } else {
- switch contentType {
- case MIMEJSON:
- return JSON
- case MIMEXML, MIMEXML2:
- return XML
- case MIMEPROTOBUF:
- return ProtoBuf
- default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
- return Form
- }
+ }
+
+ switch contentType {
+ case MIMEJSON:
+ return JSON
+ case MIMEXML, MIMEXML2:
+ return XML
+ case MIMEPROTOBUF:
+ return ProtoBuf
+ case MIMEMSGPACK, MIMEMSGPACK2:
+ return MsgPack
+ default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
+ return Form
}
}
diff --git a/binding/binding_test.go b/binding/binding_test.go
index 72f60152..d7cdf77a 100644
--- a/binding/binding_test.go
+++ b/binding/binding_test.go
@@ -12,17 +12,17 @@ import (
"github.com/gin-gonic/gin/binding/example"
"github.com/golang/protobuf/proto"
-
"github.com/stretchr/testify/assert"
+ "github.com/ugorji/go/codec"
)
type FooStruct struct {
- Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"`
+ Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
}
type FooBarStruct struct {
FooStruct
- Bar string `json:"bar" form:"bar" xml:"bar" binding:"required"`
+ Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"`
}
func TestBindingDefault(t *testing.T) {
@@ -43,6 +43,9 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, Default("POST", MIMEPROTOBUF), ProtoBuf)
assert.Equal(t, Default("PUT", MIMEPROTOBUF), ProtoBuf)
+
+ assert.Equal(t, Default("POST", MIMEMSGPACK), MsgPack)
+ assert.Equal(t, Default("PUT", MIMEMSGPACK2), MsgPack)
}
func TestBindingJSON(t *testing.T) {
@@ -121,6 +124,26 @@ func TestBindingProtoBuf(t *testing.T) {
string(data), string(data[1:]))
}
+func TestBindingMsgPack(t *testing.T) {
+ test := FooStruct{
+ Foo: "bar",
+ }
+
+ h := new(codec.MsgpackHandle)
+ assert.NotNil(t, h)
+ buf := bytes.NewBuffer([]byte{})
+ assert.NotNil(t, buf)
+ err := codec.NewEncoder(buf, h).Encode(test)
+ assert.NoError(t, err)
+
+ data := buf.Bytes()
+
+ testMsgPackBodyBinding(t,
+ MsgPack, "msgpack",
+ "/", "/",
+ string(data), string(data[1:]))
+}
+
func TestValidationFails(t *testing.T) {
var obj FooStruct
req := requestWithBody("POST", "/", `{"bar": "foo"}`)
@@ -213,6 +236,23 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba
assert.Error(t, err)
}
+func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
+ assert.Equal(t, b.Name(), name)
+
+ obj := FooStruct{}
+ req := requestWithBody("POST", path, body)
+ req.Header.Add("Content-Type", MIMEMSGPACK)
+ err := b.Bind(req, &obj)
+ assert.NoError(t, err)
+ assert.Equal(t, obj.Foo, "bar")
+
+ obj = FooStruct{}
+ req = requestWithBody("POST", badPath, badBody)
+ req.Header.Add("Content-Type", MIMEMSGPACK)
+ err = MsgPack.Bind(req, &obj)
+ assert.Error(t, err)
+}
+
func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return
diff --git a/binding/default_validator.go b/binding/default_validator.go
index 760728bb..19885f16 100644
--- a/binding/default_validator.go
+++ b/binding/default_validator.go
@@ -1,3 +1,7 @@
+// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
package binding
import (
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index 07c83751..34f12678 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -8,6 +8,7 @@ import (
"errors"
"reflect"
"strconv"
+ "time"
)
func mapForm(ptr interface{}, form map[string][]string) error {
@@ -52,6 +53,12 @@ func mapForm(ptr interface{}, form map[string][]string) error {
}
val.Field(i).Set(slice)
} else {
+ if _, isTime := structField.Interface().(time.Time); isTime {
+ if err := setTimeField(inputValue[0], typeField, structField); err != nil {
+ return err
+ }
+ continue
+ }
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err
}
@@ -140,6 +147,31 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
return err
}
+func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
+ timeFormat := structField.Tag.Get("time_format")
+ if timeFormat == "" {
+ return errors.New("Blank time format")
+ }
+
+ if val == "" {
+ value.Set(reflect.ValueOf(time.Time{}))
+ return nil
+ }
+
+ l := time.Local
+ if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
+ l = time.UTC
+ }
+
+ t, err := time.ParseInLocation(timeFormat, val, l)
+ if err != nil {
+ return err
+ }
+
+ value.Set(reflect.ValueOf(t))
+ return nil
+}
+
// Don't pass in pointers to bind to. Can lead to bugs. See:
// https://github.com/codegangsta/martini-contrib/issues/40
// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
diff --git a/binding/json.go b/binding/json.go
index 6e532443..486b9733 100644
--- a/binding/json.go
+++ b/binding/json.go
@@ -6,7 +6,6 @@ package binding
import (
"encoding/json"
-
"net/http"
)
diff --git a/binding/msgpack.go b/binding/msgpack.go
new file mode 100644
index 00000000..69367175
--- /dev/null
+++ b/binding/msgpack.go
@@ -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)
+
+}
diff --git a/binding/protobuf.go b/binding/protobuf.go
index 9f956228..c7eb84e9 100644
--- a/binding/protobuf.go
+++ b/binding/protobuf.go
@@ -5,10 +5,10 @@
package binding
import (
- "github.com/golang/protobuf/proto"
-
"io/ioutil"
"net/http"
+
+ "github.com/golang/protobuf/proto"
)
type protobufBinding struct{}
diff --git a/context.go b/context.go
index df001a40..5c4d27dc 100644
--- a/context.go
+++ b/context.go
@@ -7,17 +7,18 @@ package gin
import (
"errors"
"io"
+ "io/ioutil"
"math"
+ "mime/multipart"
"net"
"net/http"
"net/url"
"strings"
"time"
+ "github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
- "github.com/manucorporat/sse"
- "golang.org/x/net/context"
)
// Content-Type MIME of the most common data formats
@@ -31,7 +32,10 @@ const (
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
)
-const abortIndex int8 = math.MaxInt8 / 2
+const (
+ defaultMemory = 32 << 20 // 32 MB
+ abortIndex int8 = math.MaxInt8 / 2
+)
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
@@ -50,8 +54,6 @@ type Context struct {
Accepted []string
}
-var _ context.Context = &Context{}
-
/************************************/
/********** CONTEXT CREATION ********/
/************************************/
@@ -67,7 +69,7 @@ func (c *Context) reset() {
}
// Copy returns a copy of the current context that can be safely used outside the request's scope.
-// This have to be used then the context has to be passed to a goroutine.
+// This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context {
var cp = *c
cp.writermem.ResponseWriter = nil
@@ -83,13 +85,18 @@ func (c *Context) HandlerName() string {
return nameOfFunction(c.handlers.Last())
}
+// Handler returns the main handler.
+func (c *Context) Handler() HandlerFunc {
+ return c.handlers.Last()
+}
+
/************************************/
/*********** FLOW CONTROL ***********/
/************************************/
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
-// See example in github.
+// See example in GitHub.
func (c *Context) Next() {
c.index++
s := int8(len(c.handlers))
@@ -112,13 +119,20 @@ func (c *Context) Abort() {
}
// AbortWithStatus calls `Abort()` and writes the headers with the specified status code.
-// For example, a failed attempt to authentificate a request could use: context.AbortWithStatus(401).
+// For example, a failed attempt to authenticate a request could use: context.AbortWithStatus(401).
func (c *Context) AbortWithStatus(code int) {
c.Status(code)
c.Writer.WriteHeaderNow()
c.Abort()
}
+// AbortWithStatusJSON calls `Abort()` and then `JSON` internally. This method stops the chain, writes the status code and return a JSON body
+// It also sets the Content-Type as "application/json".
+func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) {
+ c.Abort()
+ c.JSON(code, jsonObj)
+}
+
// AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and
// pushes the specified error to `c.Errors`.
// See Context.Error() for more details.
@@ -154,7 +168,7 @@ func (c *Context) Error(err error) *Error {
/******** METADATA MANAGEMENT********/
/************************************/
-// Set is used to store a new key/value pair exclusivelly for this context.
+// Set is used to store a new key/value pair exclusively for this context.
// It also lazy initializes c.Keys if it was not used previously.
func (c *Context) Set(key string, value interface{}) {
if c.Keys == nil {
@@ -166,9 +180,7 @@ func (c *Context) Set(key string, value interface{}) {
// Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) {
- if c.Keys != nil {
- value, exists = c.Keys[key]
- }
+ value, exists = c.Keys[key]
return
}
@@ -180,6 +192,94 @@ func (c *Context) MustGet(key string) interface{} {
panic("Key \"" + key + "\" does not exist")
}
+// GetString returns the value associated with the key as a string.
+func (c *Context) GetString(key string) (s string) {
+ if val, ok := c.Get(key); ok && val != nil {
+ s, _ = val.(string)
+ }
+ return
+}
+
+// GetBool returns the value associated with the key as a boolean.
+func (c *Context) GetBool(key string) (b bool) {
+ if val, ok := c.Get(key); ok && val != nil {
+ b, _ = val.(bool)
+ }
+ return
+}
+
+// GetInt returns the value associated with the key as an integer.
+func (c *Context) GetInt(key string) (i int) {
+ if val, ok := c.Get(key); ok && val != nil {
+ i, _ = val.(int)
+ }
+ return
+}
+
+// GetInt64 returns the value associated with the key as an integer.
+func (c *Context) GetInt64(key string) (i64 int64) {
+ if val, ok := c.Get(key); ok && val != nil {
+ i64, _ = val.(int64)
+ }
+ return
+}
+
+// GetFloat64 returns the value associated with the key as a float64.
+func (c *Context) GetFloat64(key string) (f64 float64) {
+ if val, ok := c.Get(key); ok && val != nil {
+ f64, _ = val.(float64)
+ }
+ return
+}
+
+// GetTime returns the value associated with the key as time.
+func (c *Context) GetTime(key string) (t time.Time) {
+ if val, ok := c.Get(key); ok && val != nil {
+ t, _ = val.(time.Time)
+ }
+ return
+}
+
+// GetDuration returns the value associated with the key as a duration.
+func (c *Context) GetDuration(key string) (d time.Duration) {
+ if val, ok := c.Get(key); ok && val != nil {
+ d, _ = val.(time.Duration)
+ }
+ return
+}
+
+// GetStringSlice returns the value associated with the key as a slice of strings.
+func (c *Context) GetStringSlice(key string) (ss []string) {
+ if val, ok := c.Get(key); ok && val != nil {
+ ss, _ = val.([]string)
+ }
+ return
+}
+
+// GetStringMap returns the value associated with the key as a map of interfaces.
+func (c *Context) GetStringMap(key string) (sm map[string]interface{}) {
+ if val, ok := c.Get(key); ok && val != nil {
+ sm, _ = val.(map[string]interface{})
+ }
+ return
+}
+
+// GetStringMapString returns the value associated with the key as a map of strings.
+func (c *Context) GetStringMapString(key string) (sms map[string]string) {
+ if val, ok := c.Get(key); ok && val != nil {
+ sms, _ = val.(map[string]string)
+ }
+ return
+}
+
+// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
+func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {
+ if val, ok := c.Get(key); ok && val != nil {
+ smss, _ = val.(map[string][]string)
+ }
+ return
+}
+
/************************************/
/************ INPUT DATA ************/
/************************************/
@@ -195,7 +295,7 @@ func (c *Context) Param(key string) string {
}
// Query returns the keyed url query value if it exists,
-// othewise it returns an empty string `("")`.
+// otherwise it returns an empty string `("")`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
// GET /path?id=1234&name=Manu&value=
// c.Query("id") == "1234"
@@ -208,7 +308,7 @@ func (c *Context) Query(key string) string {
}
// DefaultQuery returns the keyed url query value if it exists,
-// othewise it returns the specified defaultValue string.
+// otherwise it returns the specified defaultValue string.
// See: Query() and GetQuery() for further information.
// GET /?name=Manu&lastname=
// c.DefaultQuery("name", "unknown") == "Manu"
@@ -223,7 +323,7 @@ func (c *Context) DefaultQuery(key, defaultValue string) string {
// GetQuery is like Query(), it returns the keyed url query value
// if it exists `(value, true)` (even when the value is an empty string),
-// othewise it returns `("", false)`.
+// otherwise it returns `("", false)`.
// It is shortcut for `c.Request.URL.Query().Get(key)`
// GET /?name=Manu&lastname=
// ("Manu", true) == c.GetQuery("name")
@@ -296,7 +396,7 @@ func (c *Context) PostFormArray(key string) []string {
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request
req.ParseForm()
- req.ParseMultipartForm(32 << 20) // 32 MB
+ req.ParseMultipartForm(defaultMemory)
if values := req.PostForm[key]; len(values) > 0 {
return values, true
}
@@ -308,6 +408,18 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false
}
+// FormFile returns the first file for the provided form key.
+func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
+ _, fh, err := c.Request.FormFile(name)
+ return fh, err
+}
+
+// MultipartForm is the parsed multipart form, including file uploads.
+func (c *Context) MultipartForm() (*multipart.Form, error) {
+ err := c.Request.ParseMultipartForm(defaultMemory)
+ return c.Request.MultipartForm, err
+}
+
// Bind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding
@@ -318,33 +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.
func (c *Context) Bind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
- return c.BindWith(obj, b)
+ return c.MustBindWith(obj, b)
}
-// BindJSON is a shortcut for c.BindWith(obj, binding.JSON)
+// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON)
func (c *Context) BindJSON(obj interface{}) error {
- return c.BindWith(obj, binding.JSON)
+ return c.MustBindWith(obj, binding.JSON)
}
-// BindWith binds the passed struct pointer using the specified binding engine.
+// MustBindWith binds the passed struct pointer using the specified binding
+// engine. It will abort the request with HTTP 400 if any error ocurrs.
// See the binding package.
-func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
- if err := b.Bind(c.Request, obj); err != nil {
+func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
+ if err = c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(400, err).SetType(ErrorTypeBind)
- return err
}
- return nil
+
+ return
+}
+
+// ShouldBindWith binds the passed struct pointer using the specified binding
+// engine.
+// See the binding package.
+func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
+ return b.Bind(c.Request, obj)
}
// ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
+// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP {
- clientIP := strings.TrimSpace(c.requestHeader("X-Real-Ip"))
- if len(clientIP) > 0 {
- return clientIP
- }
- clientIP = c.requestHeader("X-Forwarded-For")
+ clientIP := c.requestHeader("X-Forwarded-For")
if index := strings.IndexByte(clientIP, ','); index >= 0 {
clientIP = clientIP[0:index]
}
@@ -352,10 +469,22 @@ func (c *Context) ClientIP() string {
if len(clientIP) > 0 {
return clientIP
}
+ clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
+ if len(clientIP) > 0 {
+ return clientIP
+ }
}
+
+ if c.engine.AppEngine {
+ if addr := c.Request.Header.Get("X-Appengine-Remote-Addr"); addr != "" {
+ return addr
+ }
+ }
+
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
return ip
}
+
return ""
}
@@ -364,6 +493,16 @@ func (c *Context) ContentType() string {
return filterFlags(c.requestHeader("Content-Type"))
}
+// IsWebsocket returns true if the request headers indicate that a websocket
+// handshake is being initiated by the client.
+func (c *Context) IsWebsocket() bool {
+ if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
+ strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
+ return true
+ }
+ return false
+}
+
func (c *Context) requestHeader(key string) string {
if values, _ := c.Request.Header[key]; len(values) > 0 {
return values[0]
@@ -375,6 +514,19 @@ func (c *Context) requestHeader(key string) string {
/******** RESPONSE RENDERING ********/
/************************************/
+// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function
+func bodyAllowedForStatus(status int) bool {
+ switch {
+ case status >= 100 && status <= 199:
+ return false
+ case status == 204:
+ return false
+ case status == 304:
+ return false
+ }
+ return true
+}
+
func (c *Context) Status(code int) {
c.writermem.WriteHeader(code)
}
@@ -390,6 +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(
name string,
value string,
@@ -424,6 +586,13 @@ func (c *Context) Cookie(name string) (string, error) {
func (c *Context) Render(code int, r render.Render) {
c.Status(code)
+
+ if !bodyAllowedForStatus(code) {
+ r.WriteContentType(c.Writer)
+ c.Writer.WriteHeaderNow()
+ return
+ }
+
if err := r.Render(c.Writer); err != nil {
panic(err)
}
@@ -439,7 +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.
// It also sets the Content-Type as "application/json".
-// WARNING: we recommend to use this only for development propuses since printing pretty JSON is
+// WARNING: we recommend to use this only for development purposes since printing pretty JSON is
// more CPU and bandwidth consuming. Use Context.JSON() instead.
func (c *Context) IndentedJSON(code int, obj interface{}) {
c.Render(code, render.IndentedJSON{Data: obj})
@@ -448,10 +617,7 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
// JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) {
- c.Status(code)
- if err := render.WriteJSON(c.Writer, obj); err != nil {
- panic(err)
- }
+ c.Render(code, render.JSON{Data: obj})
}
// XML serializes the given struct as XML into the response body.
@@ -467,8 +633,7 @@ func (c *Context) YAML(code int, obj interface{}) {
// String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) {
- c.Status(code)
- render.WriteString(c.Writer, format, values)
+ c.Render(code, render.String{Format: format, Data: values})
}
// Redirect returns a HTTP redirect to the specific location.
diff --git a/context_appengine.go b/context_appengine.go
new file mode 100644
index 00000000..38c189a0
--- /dev/null
+++ b/context_appengine.go
@@ -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
+}
diff --git a/context_test.go b/context_test.go
index 01ee6b83..9a294c52 100644
--- a/context_test.go
+++ b/context_test.go
@@ -7,18 +7,23 @@ package gin
import (
"bytes"
"errors"
+ "fmt"
"html/template"
"mime/multipart"
"net/http"
"net/http/httptest"
+ "reflect"
"strings"
"testing"
"time"
- "github.com/manucorporat/sse"
+ "github.com/gin-contrib/sse"
"github.com/stretchr/testify/assert"
+ "golang.org/x/net/context"
)
+var _ context.Context = &Context{}
+
// Unit tests TODO
// func (c *Context) File(filepath string) {
// func (c *Context) Negotiate(code int, config Negotiate) {
@@ -38,6 +43,8 @@ func createMultipartRequest() *http.Request {
must(mw.WriteField("array", "first"))
must(mw.WriteField("array", "second"))
must(mw.WriteField("id", ""))
+ must(mw.WriteField("time_local", "31/12/2016 14:55"))
+ must(mw.WriteField("time_utc", "31/12/2016 14:55"))
req, err := http.NewRequest("POST", "/", body)
must(err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
@@ -50,6 +57,37 @@ func must(err error) {
}
}
+func TestContextFormFile(t *testing.T) {
+ buf := new(bytes.Buffer)
+ mw := multipart.NewWriter(buf)
+ w, err := mw.CreateFormFile("file", "test")
+ if assert.NoError(t, err) {
+ w.Write([]byte("test"))
+ }
+ mw.Close()
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Request, _ = http.NewRequest("POST", "/", buf)
+ c.Request.Header.Set("Content-Type", mw.FormDataContentType())
+ f, err := c.FormFile("file")
+ if assert.NoError(t, err) {
+ assert.Equal(t, "test", f.Filename)
+ }
+}
+
+func TestContextMultipartForm(t *testing.T) {
+ buf := new(bytes.Buffer)
+ mw := multipart.NewWriter(buf)
+ mw.WriteField("foo", "bar")
+ mw.Close()
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Request, _ = http.NewRequest("POST", "/", buf)
+ c.Request.Header.Set("Content-Type", mw.FormDataContentType())
+ f, err := c.MultipartForm()
+ if assert.NoError(t, err) {
+ assert.NotNil(t, f)
+ }
+}
+
func TestContextReset(t *testing.T) {
router := New()
c := router.allocateContext()
@@ -74,7 +112,7 @@ func TestContextReset(t *testing.T) {
}
func TestContextHandlers(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
assert.Nil(t, c.handlers)
assert.Nil(t, c.handlers.Last())
@@ -95,7 +133,7 @@ func TestContextHandlers(t *testing.T) {
// TestContextSetGet tests that a parameter is set correctly on the
// current context and can be retrieved using Get.
func TestContextSetGet(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("foo", "bar")
value, err := c.Get("foo")
@@ -111,7 +149,7 @@ func TestContextSetGet(t *testing.T) {
}
func TestContextSetGetValues(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("string", "this is a string")
c.Set("int32", int32(-42))
c.Set("int64", int64(42424242424242))
@@ -131,8 +169,87 @@ func TestContextSetGetValues(t *testing.T) {
}
+func TestContextGetString(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("string", "this is a string")
+ assert.Equal(t, "this is a string", c.GetString("string"))
+}
+
+func TestContextSetGetBool(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("bool", true)
+ assert.Equal(t, true, c.GetBool("bool"))
+}
+
+func TestContextGetInt(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("int", 1)
+ assert.Equal(t, 1, c.GetInt("int"))
+}
+
+func TestContextGetInt64(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("int64", int64(42424242424242))
+ assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
+}
+
+func TestContextGetFloat64(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("float64", 4.2)
+ assert.Equal(t, 4.2, c.GetFloat64("float64"))
+}
+
+func TestContextGetTime(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ t1, _ := time.Parse("1/2/2006 15:04:05", "01/01/2017 12:00:00")
+ c.Set("time", t1)
+ assert.Equal(t, t1, c.GetTime("time"))
+}
+
+func TestContextGetDuration(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("duration", time.Second)
+ assert.Equal(t, time.Second, c.GetDuration("duration"))
+}
+
+func TestContextGetStringSlice(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Set("slice", []string{"foo"})
+ assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice"))
+}
+
+func TestContextGetStringMap(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ var m = make(map[string]interface{})
+ m["foo"] = 1
+ c.Set("map", m)
+
+ assert.Equal(t, m, c.GetStringMap("map"))
+ assert.Equal(t, 1, c.GetStringMap("map")["foo"])
+}
+
+func TestContextGetStringMapString(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ var m = make(map[string]string)
+ m["foo"] = "bar"
+ c.Set("map", m)
+
+ assert.Equal(t, m, c.GetStringMapString("map"))
+ assert.Equal(t, "bar", c.GetStringMapString("map")["foo"])
+}
+
+func TestContextGetStringMapStringSlice(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ var m = make(map[string][]string)
+ m["foo"] = []string{"foo"}
+ c.Set("map", m)
+
+ assert.Equal(t, m, c.GetStringMapStringSlice("map"))
+ assert.Equal(t, []string{"foo"}, c.GetStringMapStringSlice("map")["foo"])
+}
+
func TestContextCopy(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.index = 2
c.Request, _ = http.NewRequest("POST", "/hola", nil)
c.handlers = HandlersChain{func(c *Context) {}}
@@ -151,7 +268,7 @@ func TestContextCopy(t *testing.T) {
}
func TestContextHandlerName(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName())
@@ -161,8 +278,19 @@ func handlerNameTest(c *Context) {
}
+var handlerTest HandlerFunc = func(c *Context) {
+
+}
+
+func TestContextHandler(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.handlers = HandlersChain{func(c *Context) {}, handlerTest}
+
+ assert.Equal(t, reflect.ValueOf(handlerTest).Pointer(), reflect.ValueOf(c.Handler()).Pointer())
+}
+
func TestContextQuery(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil)
value, ok := c.GetQuery("foo")
@@ -197,7 +325,7 @@ func TestContextQuery(t *testing.T) {
}
func TestContextQueryAndPostForm(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body)
c.Request.Header.Add("Content-Type", MIMEPOSTForm)
@@ -270,15 +398,18 @@ func TestContextQueryAndPostForm(t *testing.T) {
}
func TestContextPostFormMultipart(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request = createMultipartRequest()
var obj struct {
- Foo string `form:"foo"`
- Bar string `form:"bar"`
- BarAsInt int `form:"bar"`
- Array []string `form:"array"`
- ID string `form:"id"`
+ Foo string `form:"foo"`
+ Bar string `form:"bar"`
+ BarAsInt int `form:"bar"`
+ Array []string `form:"array"`
+ ID string `form:"id"`
+ TimeLocal time.Time `form:"time_local" time_format:"02/01/2006 15:04"`
+ TimeUTC time.Time `form:"time_utc" time_format:"02/01/2006 15:04" time_utc:"1"`
+ BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"`
}
assert.NoError(t, c.Bind(&obj))
assert.Equal(t, obj.Foo, "bar")
@@ -286,6 +417,11 @@ func TestContextPostFormMultipart(t *testing.T) {
assert.Equal(t, obj.BarAsInt, 10)
assert.Equal(t, obj.Array, []string{"first", "second"})
assert.Equal(t, obj.ID, "")
+ assert.Equal(t, obj.TimeLocal.Format("02/01/2006 15:04"), "31/12/2016 14:55")
+ assert.Equal(t, obj.TimeLocal.Location(), time.Local)
+ assert.Equal(t, obj.TimeUTC.Format("02/01/2006 15:04"), "31/12/2016 14:55")
+ assert.Equal(t, obj.TimeUTC.Location(), time.UTC)
+ assert.True(t, obj.BlankTime.IsZero())
value, ok := c.GetQuery("foo")
assert.False(t, ok)
@@ -334,46 +470,115 @@ func TestContextPostFormMultipart(t *testing.T) {
}
func TestContextSetCookie(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
}
+func TestContextSetCookiePathEmpty(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.SetCookie("user", "gin", 1, "", "localhost", true, true)
+ assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
+}
+
func TestContextGetCookie(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/get", nil)
c.Request.Header.Set("Cookie", "user=gin")
cookie, _ := c.Cookie("user")
assert.Equal(t, cookie, "gin")
+
+ _, err := c.Cookie("nokey")
+ assert.Error(t, err)
+}
+
+func TestContextBodyAllowedForStatus(t *testing.T) {
+ assert.Equal(t, false, bodyAllowedForStatus(102))
+ assert.Equal(t, false, bodyAllowedForStatus(204))
+ assert.Equal(t, false, bodyAllowedForStatus(304))
+ assert.Equal(t, true, bodyAllowedForStatus(500))
+}
+
+type TestPanicRender struct {
+}
+
+func (*TestPanicRender) Render(http.ResponseWriter) error {
+ return errors.New("TestPanicRender")
+}
+
+func (*TestPanicRender) WriteContentType(http.ResponseWriter) {}
+
+func TestContextRenderPanicIfErr(t *testing.T) {
+ defer func() {
+ r := recover()
+ assert.Equal(t, "TestPanicRender", fmt.Sprint(r))
+ }()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.Render(http.StatusOK, &TestPanicRender{})
+
+ assert.Fail(t, "Panic not detected")
}
// Tests that the response is serialized as JSON
// and Content-Type is set to application/json
func TestContextRenderJSON(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.JSON(201, H{"foo": "bar"})
- assert.Equal(t, w.Code, 201)
- assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
- assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
+ assert.Equal(t, 201, w.Code)
+ assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+ assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+}
+
+// Tests that no JSON is rendered if code is 204
+func TestContextRenderNoContentJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.JSON(204, H{"foo": "bar"})
+
+ assert.Equal(t, 204, w.Code)
+ assert.Equal(t, "", w.Body.String())
+ assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
// Tests that the response is serialized as JSON
// we change the content-type before
func TestContextRenderAPIJSON(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.Header("Content-Type", "application/vnd.api+json")
c.JSON(201, H{"foo": "bar"})
- assert.Equal(t, w.Code, 201)
- assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
+ assert.Equal(t, 201, w.Code)
+ assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+ assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type"))
+}
+
+// Tests that no Custom JSON is rendered if code is 204
+func TestContextRenderNoContentAPIJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.Header("Content-Type", "application/vnd.api+json")
+ c.JSON(204, H{"foo": "bar"})
+
+ assert.Equal(t, 204, w.Code)
+ assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json")
}
// Tests that the response is serialized as JSON
// and Content-Type is set to application/json
func TestContextRenderIndentedJSON(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
assert.Equal(t, w.Code, 201)
@@ -381,10 +586,23 @@ func TestContextRenderIndentedJSON(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
}
+// Tests that no Custom JSON is rendered if code is 204
+func TestContextRenderNoContentIndentedJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
+
+ assert.Equal(t, 204, w.Code)
+ assert.Equal(t, "", w.Body.String())
+ assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
+}
+
// Tests that the response executes the templates
// and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) {
- c, w, router := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, router := CreateTestContext(w)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
@@ -395,10 +613,26 @@ func TestContextRenderHTML(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
+// Tests that no HTML is rendered if code is 204
+func TestContextRenderNoContentHTML(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, router := CreateTestContext(w)
+ templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
+ router.SetHTMLTemplate(templ)
+
+ c.HTML(204, "t", H{"name": "alexandernyquist"})
+
+ assert.Equal(t, 204, w.Code)
+ assert.Equal(t, "", w.Body.String())
+ assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
+}
+
// TestContextXML tests that the response is serialized as XML
// and Content-Type is set to application/xml
func TestContextRenderXML(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.XML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201)
@@ -406,10 +640,24 @@ func TestContextRenderXML(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
}
+// Tests that no XML is rendered if code is 204
+func TestContextRenderNoContentXML(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.XML(204, H{"foo": "bar"})
+
+ assert.Equal(t, 204, w.Code)
+ assert.Equal(t, "", w.Body.String())
+ assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
+}
+
// TestContextString tests that the response is returned
// with Content-Type set to text/plain
func TestContextRenderString(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.String(201, "test %s %d", "string", 2)
assert.Equal(t, w.Code, 201)
@@ -417,10 +665,24 @@ func TestContextRenderString(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
}
+// Tests that no String is rendered if code is 204
+func TestContextRenderNoContentString(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.String(204, "test %s %d", "string", 2)
+
+ assert.Equal(t, 204, w.Code)
+ assert.Equal(t, "", w.Body.String())
+ assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
+}
+
// TestContextString tests that the response is returned
// with Content-Type set to text/html
func TestContextRenderHTMLString(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(201, "%s %d", "string", 3)
@@ -429,10 +691,25 @@ func TestContextRenderHTMLString(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
+// Tests that no HTML String is rendered if code is 204
+func TestContextRenderNoContentHTMLString(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.String(204, "%s %d", "string", 3)
+
+ assert.Equal(t, 204, w.Code)
+ assert.Equal(t, "", w.Body.String())
+ assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
+}
+
// TestContextData tests that the response can be written from `bytesting`
// with specified MIME type
func TestContextRenderData(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.Data(201, "text/csv", []byte(`foo,bar`))
assert.Equal(t, w.Code, 201)
@@ -440,8 +717,22 @@ func TestContextRenderData(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
}
+// Tests that no Custom Data is rendered if code is 204
+func TestContextRenderNoContentData(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.Data(204, "text/csv", []byte(`foo,bar`))
+
+ assert.Equal(t, 204, w.Code)
+ assert.Equal(t, "", w.Body.String())
+ assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
+}
+
func TestContextRenderSSE(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.SSEvent("float", 1.5)
c.Render(-1, sse.Event{
Id: "123",
@@ -456,7 +747,9 @@ func TestContextRenderSSE(t *testing.T) {
}
func TestContextRenderFile(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.Request, _ = http.NewRequest("GET", "/", nil)
c.File("./gin.go")
@@ -468,7 +761,9 @@ func TestContextRenderFile(t *testing.T) {
// TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/x-yaml
func TestContextRenderYAML(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.YAML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201)
@@ -477,7 +772,7 @@ func TestContextRenderYAML(t *testing.T) {
}
func TestContextHeaders(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Header("Content-Type", "text/plain")
c.Header("X-Custom", "value")
@@ -494,7 +789,9 @@ func TestContextHeaders(t *testing.T) {
// TODO
func TestContextRenderRedirectWithRelativePath(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
assert.Panics(t, func() { c.Redirect(299, "/new_path") })
assert.Panics(t, func() { c.Redirect(309, "/new_path") })
@@ -506,7 +803,9 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) {
}
func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Redirect(302, "http://google.com")
c.Writer.WriteHeaderNow()
@@ -516,7 +815,9 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
}
func TestContextRenderRedirectWith201(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Redirect(201, "/resource")
c.Writer.WriteHeaderNow()
@@ -526,7 +827,7 @@ func TestContextRenderRedirectWith201(t *testing.T) {
}
func TestContextRenderRedirectAll(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
assert.Panics(t, func() { c.Redirect(200, "/resource") })
assert.Panics(t, func() { c.Redirect(202, "/resource") })
@@ -536,8 +837,70 @@ func TestContextRenderRedirectAll(t *testing.T) {
assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
}
+func TestContextNegotiationWithJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.Request, _ = http.NewRequest("POST", "", nil)
+
+ c.Negotiate(200, Negotiate{
+ Offered: []string{MIMEJSON, MIMEXML},
+ Data: H{"foo": "bar"},
+ })
+
+ assert.Equal(t, 200, w.Code)
+ assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+ assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+}
+
+func TestContextNegotiationWithXML(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.Request, _ = http.NewRequest("POST", "", nil)
+
+ c.Negotiate(200, Negotiate{
+ Offered: []string{MIMEXML, MIMEJSON},
+ Data: H{"foo": "bar"},
+ })
+
+ assert.Equal(t, 200, w.Code)
+ assert.Equal(t, "", w.Body.String())
+ assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+}
+
+func TestContextNegotiationWithHTML(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, router := CreateTestContext(w)
+ c.Request, _ = http.NewRequest("POST", "", nil)
+ templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
+ router.SetHTMLTemplate(templ)
+
+ c.Negotiate(200, Negotiate{
+ Offered: []string{MIMEHTML},
+ Data: H{"name": "gin"},
+ HTMLName: "t",
+ })
+
+ assert.Equal(t, 200, w.Code)
+ assert.Equal(t, "Hello gin", w.Body.String())
+ assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+}
+
+func TestContextNegotiationNotSupport(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.Request, _ = http.NewRequest("POST", "", nil)
+
+ c.Negotiate(200, Negotiate{
+ Offered: []string{MIMEPOSTForm},
+ })
+
+ assert.Equal(t, 406, w.Code)
+ assert.Equal(t, c.index, abortIndex)
+ assert.True(t, c.IsAborted())
+}
+
func TestContextNegotiationFormat(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "", nil)
assert.Panics(t, func() { c.NegotiateFormat() })
@@ -546,7 +909,7 @@ func TestContextNegotiationFormat(t *testing.T) {
}
func TestContextNegotiationFormatWithAccept(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
@@ -556,7 +919,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) {
}
func TestContextNegotiationFormatCustum(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
@@ -569,7 +932,7 @@ func TestContextNegotiationFormatCustum(t *testing.T) {
}
func TestContextIsAborted(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
assert.False(t, c.IsAborted())
c.Abort()
@@ -585,7 +948,9 @@ func TestContextIsAborted(t *testing.T) {
// TestContextData tests that the response can be written from `bytesting`
// with specified MIME type
func TestContextAbortWithStatus(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.index = 4
c.AbortWithStatus(401)
@@ -595,8 +960,38 @@ func TestContextAbortWithStatus(t *testing.T) {
assert.True(t, c.IsAborted())
}
+type testJSONAbortMsg struct {
+ Foo string `json:"foo"`
+ Bar string `json:"bar"`
+}
+
+func TestContextAbortWithStatusJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.index = 4
+
+ in := new(testJSONAbortMsg)
+ in.Bar = "barValue"
+ in.Foo = "fooValue"
+
+ c.AbortWithStatusJSON(415, in)
+
+ assert.Equal(t, c.index, abortIndex)
+ assert.Equal(t, c.Writer.Status(), 415)
+ assert.Equal(t, w.Code, 415)
+ assert.True(t, c.IsAborted())
+
+ contentType := w.Header().Get("Content-Type")
+ assert.Equal(t, contentType, "application/json; charset=utf-8")
+
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(w.Body)
+ jsonStringBody := buf.String()
+ assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
+}
+
func TestContextError(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
assert.Empty(t, c.Errors)
c.Error(errors.New("first error"))
@@ -622,7 +1017,7 @@ func TestContextError(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("interno 0")).SetType(ErrorTypePrivate)
@@ -636,7 +1031,9 @@ func TestContextTypedError(t *testing.T) {
}
func TestContextAbortWithError(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
assert.Equal(t, w.Code, 401)
@@ -645,27 +1042,37 @@ func TestContextAbortWithError(t *testing.T) {
}
func TestContextClientIP(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
+ c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.RemoteAddr = " 40.40.40.40:42123 "
- assert.Equal(t, c.ClientIP(), "10.10.10.10")
-
- c.Request.Header.Del("X-Real-IP")
- assert.Equal(t, c.ClientIP(), "20.20.20.20")
-
- c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ")
- assert.Equal(t, c.ClientIP(), "30.30.30.30")
+ assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For")
- assert.Equal(t, c.ClientIP(), "40.40.40.40")
+ assert.Equal(t, "10.10.10.10", c.ClientIP())
+
+ c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ")
+ assert.Equal(t, "30.30.30.30", c.ClientIP())
+
+ c.Request.Header.Del("X-Forwarded-For")
+ c.Request.Header.Del("X-Real-IP")
+ c.engine.AppEngine = true
+ assert.Equal(t, "50.50.50.50", c.ClientIP())
+
+ c.Request.Header.Del("X-Appengine-Remote-Addr")
+ assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+ // no port
+ c.Request.RemoteAddr = "50.50.50.50"
+ assert.Equal(t, "", c.ClientIP())
}
func TestContextContentType(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
@@ -673,7 +1080,7 @@ func TestContextContentType(t *testing.T) {
}
func TestContextAutoBindJSON(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON)
@@ -688,7 +1095,9 @@ func TestContextAutoBindJSON(t *testing.T) {
}
func TestContextBindWithJSON(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
@@ -703,7 +1112,9 @@ func TestContextBindWithJSON(t *testing.T) {
}
func TestContextBadAutoBind(t *testing.T) {
- c, w, _ := CreateTestContext()
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct {
@@ -722,7 +1133,7 @@ func TestContextBadAutoBind(t *testing.T) {
}
func TestContextGolangContext(t *testing.T) {
- c, _, _ := CreateTestContext()
+ c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
assert.NoError(t, c.Err())
assert.Nil(t, c.Done())
@@ -736,3 +1147,45 @@ func TestContextGolangContext(t *testing.T) {
assert.Equal(t, c.Value("foo"), "bar")
assert.Nil(t, c.Value(1))
}
+
+func TestWebsocketsRequired(t *testing.T) {
+ // Example request from spec: https://tools.ietf.org/html/rfc6455#section-1.2
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Request, _ = http.NewRequest("GET", "/chat", nil)
+ c.Request.Header.Set("Host", "server.example.com")
+ c.Request.Header.Set("Upgrade", "websocket")
+ c.Request.Header.Set("Connection", "Upgrade")
+ c.Request.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
+ c.Request.Header.Set("Origin", "http://example.com")
+ c.Request.Header.Set("Sec-WebSocket-Protocol", "chat, superchat")
+ c.Request.Header.Set("Sec-WebSocket-Version", "13")
+
+ assert.True(t, c.IsWebsocket())
+
+ // Normal request, no websocket required.
+ c, _ = CreateTestContext(httptest.NewRecorder())
+ c.Request, _ = http.NewRequest("GET", "/chat", nil)
+ c.Request.Header.Set("Host", "server.example.com")
+
+ assert.False(t, c.IsWebsocket())
+}
+
+func TestGetRequestHeaderValue(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ c.Request, _ = http.NewRequest("GET", "/chat", nil)
+ c.Request.Header.Set("Gin-Version", "1.0.0")
+
+ assert.Equal(t, "1.0.0", c.GetHeader("Gin-Version"))
+ assert.Equal(t, "", c.GetHeader("Connection"))
+}
+
+func TestContextGetRawData(t *testing.T) {
+ c, _ := CreateTestContext(httptest.NewRecorder())
+ body := bytes.NewBufferString("Fetch binary post data")
+ c.Request, _ = http.NewRequest("POST", "/", body)
+ c.Request.Header.Add("Content-Type", MIMEPOSTForm)
+
+ data, err := c.GetRawData()
+ assert.Nil(t, err)
+ assert.Equal(t, "Fetch binary post data", string(data))
+}
diff --git a/debug_test.go b/debug_test.go
index deceaa6e..c30081c5 100644
--- a/debug_test.go
+++ b/debug_test.go
@@ -7,6 +7,7 @@ package gin
import (
"bytes"
"errors"
+ "html/template"
"io"
"log"
"os"
@@ -66,6 +67,25 @@ func TestDebugPrintRoutes(t *testing.T) {
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, w.String())
}
+func TestDebugPrintLoadTemplate(t *testing.T) {
+ var w bytes.Buffer
+ setup(&w)
+ defer teardown()
+
+ templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl"))
+ debugPrintLoadTemplate(templ)
+ assert.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) {
SetMode(DebugMode)
log.SetOutput(w)
diff --git a/deprecated.go b/deprecated.go
index 0488a9b0..27e8f558 100644
--- a/deprecated.go
+++ b/deprecated.go
@@ -4,9 +4,22 @@
package gin
-import "log"
+import (
+ "github.com/gin-gonic/gin/binding"
+ "log"
+)
func (c *Context) GetCookie(name string) (string, error) {
log.Println("GetCookie() method is deprecated. Use Cookie() instead.")
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)
+}
diff --git a/errors.go b/errors.go
index 7716bfae..896af6fc 100644
--- a/errors.go
+++ b/errors.go
@@ -72,7 +72,7 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
}
// Implements the error interface
-func (msg *Error) Error() string {
+func (msg Error) Error() string {
return msg.Err.Error()
}
@@ -80,7 +80,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0
}
-// Returns a readonly copy filterd the byte.
+// Returns a readonly copy filtered the byte.
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic
func (a errorMsgs) ByType(typ ErrorType) errorMsgs {
if len(a) == 0 {
diff --git a/errors_test.go b/errors_test.go
index c9a3407b..1aa0cdde 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -54,6 +54,13 @@ func TestError(t *testing.T) {
"status": "200",
"data": "some data",
})
+
+ type customError struct {
+ status string
+ data string
+ }
+ err.SetMeta(customError{status: "200", data: "other data"})
+ assert.Equal(t, err.JSON(), customError{status: "200", data: "other data"})
}
func TestErrorSlice(t *testing.T) {
diff --git a/examples/app-engine/hello.go b/examples/app-engine/hello.go
index a5e17962..da7e4ae4 100644
--- a/examples/app-engine/hello.go
+++ b/examples/app-engine/hello.go
@@ -1,8 +1,9 @@
package hello
import (
- "github.com/gin-gonic/gin"
"net/http"
+
+ "github.com/gin-gonic/gin"
)
// This function's name is a must. App Engine uses it to drive the requests properly.
diff --git a/examples/auto-tls/example1.go b/examples/auto-tls/example1.go
new file mode 100644
index 00000000..fa9f4008
--- /dev/null
+++ b/examples/auto-tls/example1.go
@@ -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"))
+}
diff --git a/examples/auto-tls/example2.go b/examples/auto-tls/example2.go
new file mode 100644
index 00000000..ab8b81e7
--- /dev/null
+++ b/examples/auto-tls/example2.go
@@ -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))
+}
diff --git a/examples/basic/main.go b/examples/basic/main.go
index 80f2bd3c..984c06ab 100644
--- a/examples/basic/main.go
+++ b/examples/basic/main.go
@@ -7,6 +7,8 @@ import (
var DB = make(map[string]string)
func main() {
+ // Disable Console Color
+ // gin.DisableConsoleColor()
r := gin.Default()
// Ping test
diff --git a/examples/graceful-shutdown/close/server.go b/examples/graceful-shutdown/close/server.go
new file mode 100644
index 00000000..54778393
--- /dev/null
+++ b/examples/graceful-shutdown/close/server.go
@@ -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")
+}
diff --git a/examples/graceful-shutdown/graceful-shutdown/server.go b/examples/graceful-shutdown/graceful-shutdown/server.go
new file mode 100644
index 00000000..060de081
--- /dev/null
+++ b/examples/graceful-shutdown/graceful-shutdown/server.go
@@ -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")
+}
diff --git a/examples/realtime-advanced/Makefile b/examples/realtime-advanced/Makefile
new file mode 100644
index 00000000..104ce809
--- /dev/null
+++ b/examples/realtime-advanced/Makefile
@@ -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
diff --git a/examples/realtime-advanced/routes.go b/examples/realtime-advanced/routes.go
index b1877565..86da9bea 100644
--- a/examples/realtime-advanced/routes.go
+++ b/examples/realtime-advanced/routes.go
@@ -11,7 +11,6 @@ import (
)
func rateLimit(c *gin.Context) {
-
ip := c.ClientIP()
value := int(ips.Add(ip, 1))
if value%50 == 0 {
diff --git a/examples/realtime-advanced/stats.go b/examples/realtime-advanced/stats.go
index 554ab86a..4bca3ae4 100644
--- a/examples/realtime-advanced/stats.go
+++ b/examples/realtime-advanced/stats.go
@@ -8,11 +8,13 @@ import (
"github.com/manucorporat/stats"
)
-var ips = stats.New()
-var messages = stats.New()
-var users = stats.New()
-var mutexStats sync.RWMutex
-var savedStats map[string]uint64
+var (
+ ips = stats.New()
+ messages = stats.New()
+ users = stats.New()
+ mutexStats sync.RWMutex
+ savedStats map[string]uint64
+)
func statsWorker() {
c := time.Tick(1 * time.Second)
diff --git a/examples/realtime-chat/Makefile b/examples/realtime-chat/Makefile
new file mode 100644
index 00000000..dea583df
--- /dev/null
+++ b/examples/realtime-chat/Makefile
@@ -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
diff --git a/examples/upload-file/multiple/main.go b/examples/upload-file/multiple/main.go
new file mode 100644
index 00000000..2f4e9b52
--- /dev/null
+++ b/examples/upload-file/multiple/main.go
@@ -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")
+}
diff --git a/examples/upload-file/multiple/public/index.html b/examples/upload-file/multiple/public/index.html
new file mode 100644
index 00000000..b8463601
--- /dev/null
+++ b/examples/upload-file/multiple/public/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Multiple file upload
+
+
+Upload multiple files with fields
+
+
+
+
diff --git a/examples/upload-file/single/main.go b/examples/upload-file/single/main.go
new file mode 100644
index 00000000..9acf5009
--- /dev/null
+++ b/examples/upload-file/single/main.go
@@ -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")
+}
diff --git a/examples/upload-file/single/public/index.html b/examples/upload-file/single/public/index.html
new file mode 100644
index 00000000..b0c2a808
--- /dev/null
+++ b/examples/upload-file/single/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Single file upload
+
+
+Upload single file with fields
+
+
+
diff --git a/fixtures/basic/hello.tmpl b/fixtures/basic/hello.tmpl
new file mode 100644
index 00000000..0767ef3f
--- /dev/null
+++ b/fixtures/basic/hello.tmpl
@@ -0,0 +1 @@
+Hello {[{.name}]}
\ No newline at end of file
diff --git a/fixtures/basic/raw.tmpl b/fixtures/basic/raw.tmpl
new file mode 100644
index 00000000..8bc75703
--- /dev/null
+++ b/fixtures/basic/raw.tmpl
@@ -0,0 +1 @@
+Date: {[{.now | formatAsDate}]}
diff --git a/fs.go b/fs.go
index 6af3ded5..12645826 100644
--- a/fs.go
+++ b/fs.go
@@ -1,3 +1,7 @@
+// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
package gin
import (
@@ -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().
// 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.
diff --git a/gin.go b/gin.go
index 60f4ab29..c4118a4e 100644
--- a/gin.go
+++ b/gin.go
@@ -15,10 +15,11 @@ import (
)
// Version is Framework's version
-const Version = "v1.0rc2"
+const Version = "v1.2"
var default404Body = []byte("404 page not found")
var default405Body = []byte("405 method not allowed")
+var defaultAppEngine bool
type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc
@@ -44,7 +45,9 @@ type (
// Create an instance of Engine, by using New() or Default()
Engine struct {
RouterGroup
+ delims render.Delims
HTMLRender render.HTMLRender
+ FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
@@ -78,6 +81,17 @@ type (
// handler.
HandleMethodNotAllowed bool
ForwardedByClientIP bool
+
+ // #726 #755 If enabled, it will thrust some headers starting with
+ // 'X-AppEngine...' for better integration with that PaaS.
+ AppEngine bool
+
+ // If enabled, the url.RawPath will be used to find parameters.
+ UseRawPath bool
+ // If true, the path value will be unescaped.
+ // If UseRawPath is false (by default), the UnescapePathValues effectively is true,
+ // as url.Path gonna be used, which is already unescaped.
+ UnescapePathValues bool
}
)
@@ -89,6 +103,8 @@ var _ IRouter = &Engine{}
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
+// - UseRawPath: false
+// - UnescapePathValues: true
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
@@ -97,11 +113,16 @@ func New() *Engine {
basePath: "/",
root: true,
},
+ FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
+ AppEngine: defaultAppEngine,
+ UseRawPath: false,
+ UnescapePathValues: true,
trees: make(methodTrees, 0, 9),
+ delims: render.Delims{"{{", "}}"},
}
engine.RouterGroup.engine = engine
engine.pool.New = func() interface{} {
@@ -121,21 +142,26 @@ func (engine *Engine) allocateContext() *Context {
return &Context{engine: engine}
}
+func (engine *Engine) Delims(left, right string) *Engine {
+ engine.delims = render.Delims{left, right}
+ return engine
+}
+
func (engine *Engine) LoadHTMLGlob(pattern string) {
if IsDebugging() {
- debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern)))
- engine.HTMLRender = render.HTMLDebug{Glob: pattern}
+ debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern)))
+ engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
} else {
- templ := template.Must(template.ParseGlob(pattern))
+ templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern))
engine.SetHTMLTemplate(templ)
}
}
func (engine *Engine) LoadHTMLFiles(files ...string) {
if IsDebugging() {
- engine.HTMLRender = render.HTMLDebug{Files: files}
+ engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims}
} else {
- templ := template.Must(template.ParseFiles(files...))
+ templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFiles(files...))
engine.SetHTMLTemplate(templ)
}
}
@@ -144,7 +170,12 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
if len(engine.trees) > 0 {
debugPrintWARNINGSetHTMLTemplate()
}
- engine.HTMLRender = render.HTMLProduction{Template: templ}
+
+ engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)}
+}
+
+func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
+ engine.FuncMap = funcMap
}
// NoRoute adds handlers for NoRoute. It return a 404 code by default.
@@ -267,9 +298,26 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.pool.Put(c)
}
+// Re-enter a context that has been rewritten.
+// This can be done by setting c.Request.Path to your new target.
+// Disclaimer: You can loop yourself to death with this, use wisely.
+func (engine *Engine) HandleContext(c *Context) {
+ c.reset()
+ engine.handleHTTPRequest(c)
+ engine.pool.Put(c)
+}
+
func (engine *Engine) handleHTTPRequest(context *Context) {
httpMethod := context.Request.Method
- path := context.Request.URL.Path
+ var path string
+ var unescape bool
+ if engine.UseRawPath && len(context.Request.URL.RawPath) > 0 {
+ path = context.Request.URL.RawPath
+ unescape = engine.UnescapePathValues
+ } else {
+ path = context.Request.URL.Path
+ unescape = false
+ }
// Find root of the tree for the given HTTP method
t := engine.trees
@@ -277,15 +325,15 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
if t[i].method == httpMethod {
root := t[i].root
// Find route in tree
- handlers, params, tsr := root.getValue(path, context.Params)
+ handlers, params, tsr := root.getValue(path, context.Params, unescape)
if handlers != nil {
context.handlers = handlers
context.Params = params
context.Next()
context.writermem.WriteHeaderNow()
return
-
- } else if httpMethod != "CONNECT" && path != "/" {
+ }
+ if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(context)
return
@@ -302,7 +350,7 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method != httpMethod {
- if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil {
+ if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
context.handlers = engine.allNoMethod
serveError(context, 405, default405Body)
return
diff --git a/ginS/README.md b/ginS/README.md
index ac563a2c..ef9225e4 100644
--- a/ginS/README.md
+++ b/ginS/README.md
@@ -1,4 +1,4 @@
-#Gin Default Server
+# Gin Default Server
This is API experiment for Gin.
diff --git a/gin_integration_test.go b/gin_integration_test.go
index 85216970..f45dd6c1 100644
--- a/gin_integration_test.go
+++ b/gin_integration_test.go
@@ -1,3 +1,7 @@
+// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
package gin
import (
@@ -6,18 +10,18 @@ import (
"io/ioutil"
"net"
"net/http"
+ "net/http/httptest"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
- "net/http/httptest"
)
func testRequest(t *testing.T, url string) {
resp, err := http.Get(url)
- defer resp.Body.Close()
assert.NoError(t, err)
+ defer resp.Body.Close()
body, ioerr := ioutil.ReadAll(resp.Body)
assert.NoError(t, ioerr)
@@ -115,17 +119,17 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
testRequest(t, ts.URL+"/example")
}
-func TestWithHttptestWithSpecifiedPort(t *testing.T) {
- router := New()
- router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+// func TestWithHttptestWithSpecifiedPort(t *testing.T) {
+// router := New()
+// router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
- l, _ := net.Listen("tcp", ":8033")
- ts := httptest.Server{
- Listener: l,
- Config: &http.Server{Handler: router},
- }
- ts.Start()
- defer ts.Close()
+// l, _ := net.Listen("tcp", ":8033")
+// ts := httptest.Server{
+// Listener: l,
+// Config: &http.Server{Handler: router},
+// }
+// ts.Start()
+// defer ts.Close()
- testRequest(t, "http://localhost:8033/example")
-}
+// testRequest(t, "http://localhost:8033/example")
+// }
diff --git a/gin_test.go b/gin_test.go
index cc24bc92..bdf5a9a9 100644
--- a/gin_test.go
+++ b/gin_test.go
@@ -5,14 +5,98 @@
package gin
import (
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "net/http"
"reflect"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
)
+func formatAsDate(t time.Time) string {
+ year, month, day := t.Date()
+ return fmt.Sprintf("%d/%02d/%02d", year, month, day)
+}
+
+func setupHTMLFiles(t *testing.T) func() {
+ go func() {
+ SetMode(TestMode)
+ router := New()
+ router.Delims("{[{", "}]}")
+ router.SetFuncMap(template.FuncMap{
+ "formatAsDate": formatAsDate,
+ })
+ router.LoadHTMLFiles("./fixtures/basic/hello.tmpl", "./fixtures/basic/raw.tmpl")
+ router.GET("/test", func(c *Context) {
+ c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
+ })
+ router.GET("/raw", func(c *Context) {
+ c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
+ "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
+ })
+ })
+ router.Run(":8888")
+ }()
+ t.Log("waiting 1 second for server startup")
+ time.Sleep(1 * time.Second)
+ return func() {}
+}
+
+func setupHTMLGlob(t *testing.T) func() {
+ go func() {
+ SetMode(DebugMode)
+ router := New()
+ router.Delims("{[{", "}]}")
+ router.SetFuncMap(template.FuncMap{
+ "formatAsDate": formatAsDate,
+ })
+ router.LoadHTMLGlob("./fixtures/basic/*")
+ router.GET("/test", func(c *Context) {
+ c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
+ })
+ router.GET("/raw", func(c *Context) {
+ c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
+ "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
+ })
+ })
+ router.Run(":8888")
+ }()
+ t.Log("waiting 1 second for server startup")
+ time.Sleep(1 * time.Second)
+ return func() {}
+}
+
//TODO
-// func (engine *Engine) LoadHTMLGlob(pattern string) {
+func TestLoadHTMLGlob(t *testing.T) {
+ td := setupHTMLGlob(t)
+ res, err := http.Get("http://127.0.0.1:8888/test")
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ resp, _ := ioutil.ReadAll(res.Body)
+ assert.Equal(t, "Hello world
", string(resp[:]))
+
+ td()
+}
+
+func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
+ time.Now()
+ td := setupHTMLGlob(t)
+ res, err := http.Get("http://127.0.0.1:8888/raw")
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ resp, _ := ioutil.ReadAll(res.Body)
+ assert.Equal(t, "Date: 2017/07/01\n", string(resp[:]))
+
+ td()
+}
+
// func (engine *Engine) LoadHTMLFiles(files ...string) {
// func (engine *Engine) RunTLS(addr string, cert string, key string) error {
@@ -42,6 +126,32 @@ func TestCreateEngine(t *testing.T) {
// SetMode(TestMode)
// }
+func TestLoadHTMLFiles(t *testing.T) {
+ td := setupHTMLFiles(t)
+ res, err := http.Get("http://127.0.0.1:8888/test")
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ resp, _ := ioutil.ReadAll(res.Body)
+ assert.Equal(t, "Hello world
", 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) {
}
diff --git a/helpers_test.go b/helpers_test.go
deleted file mode 100644
index 7d8020c3..00000000
--- a/helpers_test.go
+++ /dev/null
@@ -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
-}
diff --git a/logger.go b/logger.go
index 7c2a72be..dc6f1415 100644
--- a/logger.go
+++ b/logger.go
@@ -14,16 +14,21 @@ import (
)
var (
- green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
- white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
- yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
- red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
- blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
- magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
- cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
- reset = string([]byte{27, 91, 48, 109})
+ green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
+ white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
+ yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
+ red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
+ blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
+ magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
+ cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
+ reset = string([]byte{27, 91, 48, 109})
+ disableColor = false
)
+func DisableConsoleColor() {
+ disableColor = true
+}
+
func ErrorLogger() HandlerFunc {
return ErrorLoggerT(ErrorTypeAny)
}
@@ -49,7 +54,9 @@ func Logger() HandlerFunc {
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
isTerm := true
- if w, ok := out.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) {
+ if w, ok := out.(*os.File); !ok ||
+ (os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) ||
+ disableColor {
isTerm = false
}
@@ -87,12 +94,12 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
}
comment := c.Errors.ByType(ErrorTypePrivate).String()
- fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s",
+ fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %s %-7s %s\n%s",
end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, reset,
latency,
clientIP,
- methodColor, reset, method,
+ methodColor, method, reset,
path,
comment,
)
diff --git a/logger_test.go b/logger_test.go
index 2ad1f474..15d6ee9c 100644
--- a/logger_test.go
+++ b/logger_test.go
@@ -36,37 +36,43 @@ func TestLogger(t *testing.T) {
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
-
+ buffer.Reset()
performRequest(router, "POST", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example")
+ buffer.Reset()
performRequest(router, "PUT", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example")
+ buffer.Reset()
performRequest(router, "DELETE", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example")
+ buffer.Reset()
performRequest(router, "PATCH", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example")
+ buffer.Reset()
performRequest(router, "HEAD", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example")
+ buffer.Reset()
performRequest(router, "OPTIONS", "/example")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example")
+ buffer.Reset()
performRequest(router, "GET", "/notfound")
assert.Contains(t, buffer.String(), "404")
assert.Contains(t, buffer.String(), "GET")
@@ -107,16 +113,16 @@ func TestErrorLogger(t *testing.T) {
})
w := performRequest(router, "GET", "/error")
- assert.Equal(t, w.Code, 200)
- assert.Equal(t, w.Body.String(), "{\"error\":\"this is an error\"}\n")
+ assert.Equal(t, 200, w.Code)
+ assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort")
- assert.Equal(t, w.Code, 401)
- assert.Equal(t, w.Body.String(), "{\"error\":\"no authorized\"}\n")
+ assert.Equal(t, 401, w.Code)
+ assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print")
- assert.Equal(t, w.Code, 500)
- assert.Equal(t, w.Body.String(), "hola!{\"error\":\"this is an error\"}\n")
+ assert.Equal(t, 500, w.Code)
+ assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
}
func TestSkippingPaths(t *testing.T) {
@@ -129,6 +135,14 @@ func TestSkippingPaths(t *testing.T) {
performRequest(router, "GET", "/logged")
assert.Contains(t, buffer.String(), "200")
+ buffer.Reset()
performRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "")
}
+
+func TestDisableConsoleColor(t *testing.T) {
+ New()
+ assert.False(t, disableColor)
+ DisableConsoleColor()
+ assert.True(t, disableColor)
+}
diff --git a/middleware_test.go b/middleware_test.go
index 3101d523..5572e790 100644
--- a/middleware_test.go
+++ b/middleware_test.go
@@ -7,10 +7,9 @@ package gin
import (
"errors"
"strings"
-
"testing"
- "github.com/manucorporat/sse"
+ "github.com/gin-contrib/sse"
"github.com/stretchr/testify/assert"
)
@@ -245,6 +244,6 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/")
- assert.Equal(t, w.Code, 400)
- assert.Equal(t, strings.Replace(w.Body.String(), " ", "", -1), strings.Replace("hola\n{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1))
+ assert.Equal(t, 400, w.Code)
+ assert.Equal(t, strings.Replace("hola\n{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
}
diff --git a/mode.go b/mode.go
index c600b7b5..e24dbdc2 100644
--- a/mode.go
+++ b/mode.go
@@ -19,9 +19,9 @@ const (
TestMode string = "test"
)
const (
- debugCode = iota
- releaseCode
- testCode
+ debugCode = iota
+ releaseCode
+ testCode
)
// DefaultWriter is the default io.Writer used the Gin for debug output and
diff --git a/path.go b/path.go
index 43cdd047..d7e7458b 100644
--- a/path.go
+++ b/path.go
@@ -1,7 +1,7 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Based on the path package, Copyright 2009 The Go Authors.
// Use of this source code is governed by a BSD-style license that can be found
-// in the LICENSE file.
+// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE.
package gin
diff --git a/path_test.go b/path_test.go
index 01cb758a..bf2e5f62 100644
--- a/path_test.go
+++ b/path_test.go
@@ -1,7 +1,7 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Based on the path package, Copyright 2009 The Go Authors.
// Use of this source code is governed by a BSD-style license that can be found
-// in the LICENSE file.
+// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
package gin
diff --git a/render/data.go b/render/data.go
index efa75d55..c296042c 100644
--- a/render/data.go
+++ b/render/data.go
@@ -11,10 +11,13 @@ type Data struct {
Data []byte
}
-func (r Data) Render(w http.ResponseWriter) error {
- if len(r.ContentType) > 0 {
- w.Header()["Content-Type"] = []string{r.ContentType}
- }
- w.Write(r.Data)
- return nil
+// Render (Data) writes data with custom ContentType
+func (r Data) Render(w http.ResponseWriter) (err error) {
+ r.WriteContentType(w)
+ _, err = w.Write(r.Data)
+ return
+}
+
+func (r Data) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, []string{r.ContentType})
}
diff --git a/render/html.go b/render/html.go
index 8bfb23ac..cf91219c 100644
--- a/render/html.go
+++ b/render/html.go
@@ -10,17 +10,25 @@ import (
)
type (
+ Delims struct {
+ Left string
+ Right string
+ }
+
HTMLRender interface {
Instance(string, interface{}) Render
}
HTMLProduction struct {
Template *template.Template
+ Delims Delims
}
HTMLDebug struct {
- Files []string
- Glob string
+ Files []string
+ Glob string
+ Delims Delims
+ FuncMap template.FuncMap
}
HTML struct {
@@ -48,19 +56,27 @@ func (r HTMLDebug) Instance(name string, data interface{}) Render {
}
}
func (r HTMLDebug) loadTemplate() *template.Template {
+ if r.FuncMap == nil {
+ r.FuncMap = template.FuncMap{}
+ }
if len(r.Files) > 0 {
- return template.Must(template.ParseFiles(r.Files...))
+ return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFiles(r.Files...))
}
if len(r.Glob) > 0 {
- return template.Must(template.ParseGlob(r.Glob))
+ return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
}
panic("the HTML debug render was created without files or glob pattern")
}
func (r HTML) Render(w http.ResponseWriter) error {
- writeContentType(w, htmlContentType)
+ r.WriteContentType(w)
+
if len(r.Name) == 0 {
return r.Template.Execute(w, r.Data)
}
return r.Template.ExecuteTemplate(w, r.Name, r.Data)
}
+
+func (r HTML) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, htmlContentType)
+}
diff --git a/render/json.go b/render/json.go
index 32e6058d..3ee8b132 100644
--- a/render/json.go
+++ b/render/json.go
@@ -21,12 +21,29 @@ type (
var jsonContentType = []string{"application/json; charset=utf-8"}
-func (r JSON) Render(w http.ResponseWriter) error {
- return WriteJSON(w, r.Data)
+func (r JSON) Render(w http.ResponseWriter) (err error) {
+ if err = WriteJSON(w, r.Data); err != nil {
+ panic(err)
+ }
+ return
+}
+
+func (r JSON) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, jsonContentType)
+}
+
+func WriteJSON(w http.ResponseWriter, obj interface{}) error {
+ writeContentType(w, jsonContentType)
+ jsonBytes, err := json.Marshal(obj)
+ if err != nil {
+ return err
+ }
+ w.Write(jsonBytes)
+ return nil
}
func (r IndentedJSON) Render(w http.ResponseWriter) error {
- writeContentType(w, jsonContentType)
+ r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
if err != nil {
return err
@@ -35,7 +52,6 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error {
return nil
}
-func WriteJSON(w http.ResponseWriter, obj interface{}) error {
+func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
- return json.NewEncoder(w).Encode(obj)
}
diff --git a/render/msgpack.go b/render/msgpack.go
new file mode 100644
index 00000000..e6c13e58
--- /dev/null
+++ b/render/msgpack.go
@@ -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)
+}
diff --git a/render/redirect.go b/render/redirect.go
index bd48d7d8..f874a351 100644
--- a/render/redirect.go
+++ b/render/redirect.go
@@ -22,3 +22,5 @@ func (r Redirect) Render(w http.ResponseWriter) error {
http.Redirect(w, r.Request, r.Location, r.Code)
return nil
}
+
+func (r Redirect) WriteContentType(http.ResponseWriter) {}
diff --git a/render/render.go b/render/render.go
index 3808666a..46291421 100644
--- a/render/render.go
+++ b/render/render.go
@@ -8,6 +8,7 @@ import "net/http"
type Render interface {
Render(http.ResponseWriter) error
+ WriteContentType(w http.ResponseWriter)
}
var (
@@ -21,6 +22,8 @@ var (
_ HTMLRender = HTMLDebug{}
_ HTMLRender = HTMLProduction{}
_ Render = YAML{}
+ _ Render = MsgPack{}
+ _ Render = MsgPack{}
)
func writeContentType(w http.ResponseWriter, value []string) {
diff --git a/render/render_test.go b/render/render_test.go
index 7a6ffb7d..c48235c3 100644
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -5,17 +5,40 @@
package render
import (
+ "bytes"
"encoding/xml"
"html/template"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/ugorji/go/codec"
)
// TODO unit tests
// test errors
+func TestRenderMsgPack(t *testing.T) {
+ w := httptest.NewRecorder()
+ data := map[string]interface{}{
+ "foo": "bar",
+ }
+
+ err := (MsgPack{data}).Render(w)
+
+ assert.NoError(t, err)
+
+ h := new(codec.MsgpackHandle)
+ assert.NotNil(t, h)
+ buf := bytes.NewBuffer([]byte{})
+ assert.NotNil(t, buf)
+ err = codec.NewEncoder(buf, h).Encode(data)
+
+ assert.NoError(t, err)
+ assert.Equal(t, w.Body.String(), string(buf.Bytes()))
+ assert.Equal(t, w.Header().Get("Content-Type"), "application/msgpack; charset=utf-8")
+}
+
func TestRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]interface{}{
@@ -25,8 +48,8 @@ func TestRenderJSON(t *testing.T) {
err := (JSON{data}).Render(w)
assert.NoError(t, err)
- assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
- assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
+ assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+ assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderIndentedJSON(t *testing.T) {
diff --git a/render/text.go b/render/text.go
index 5a9e280b..74cd26be 100644
--- a/render/text.go
+++ b/render/text.go
@@ -22,9 +22,12 @@ func (r String) Render(w http.ResponseWriter) error {
return nil
}
+func (r String) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, plainContentType)
+}
+
func WriteString(w http.ResponseWriter, format string, data []interface{}) {
writeContentType(w, plainContentType)
-
if len(data) > 0 {
fmt.Fprintf(w, format, data...)
} else {
diff --git a/render/xml.go b/render/xml.go
index be22e6f2..cff1ac3e 100644
--- a/render/xml.go
+++ b/render/xml.go
@@ -16,6 +16,10 @@ type XML struct {
var xmlContentType = []string{"application/xml; charset=utf-8"}
func (r XML) Render(w http.ResponseWriter) error {
- writeContentType(w, xmlContentType)
+ r.WriteContentType(w)
return xml.NewEncoder(w).Encode(r.Data)
}
+
+func (r XML) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, xmlContentType)
+}
diff --git a/render/yaml.go b/render/yaml.go
index 46937d88..25d0ebd4 100644
--- a/render/yaml.go
+++ b/render/yaml.go
@@ -17,7 +17,7 @@ type YAML struct {
var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
func (r YAML) Render(w http.ResponseWriter) error {
- writeContentType(w, yamlContentType)
+ r.WriteContentType(w)
bytes, err := yaml.Marshal(r.Data)
if err != nil {
@@ -27,3 +27,7 @@ func (r YAML) Render(w http.ResponseWriter) error {
w.Write(bytes)
return nil
}
+
+func (r YAML) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, yamlContentType)
+}
diff --git a/routes_test.go b/routes_test.go
index 32f00983..7464d5d0 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -400,3 +400,42 @@ func TestRouterNotFound(t *testing.T) {
w = performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 404)
}
+
+func TestRouteRawPath(t *testing.T) {
+ route := New()
+ route.UseRawPath = true
+
+ route.POST("/project/:name/build/:num", func(c *Context) {
+ name := c.Params.ByName("name")
+ num := c.Params.ByName("num")
+
+ assert.Equal(t, c.Param("name"), name)
+ assert.Equal(t, c.Param("num"), num)
+
+ assert.Equal(t, "Some/Other/Project", name)
+ assert.Equal(t, "222", num)
+ })
+
+ w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222")
+ assert.Equal(t, w.Code, 200)
+}
+
+func TestRouteRawPathNoUnescape(t *testing.T) {
+ route := New()
+ route.UseRawPath = true
+ route.UnescapePathValues = false
+
+ route.POST("/project/:name/build/:num", func(c *Context) {
+ name := c.Params.ByName("name")
+ num := c.Params.ByName("num")
+
+ assert.Equal(t, c.Param("name"), name)
+ assert.Equal(t, c.Param("num"), num)
+
+ assert.Equal(t, "Some%2FOther%2FProject", name)
+ assert.Equal(t, "333", num)
+ })
+
+ w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333")
+ assert.Equal(t, w.Code, 200)
+}
diff --git a/test_helpers.go b/test_helpers.go
new file mode 100644
index 00000000..e7dd55f7
--- /dev/null
+++ b/test_helpers.go
@@ -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
+}
diff --git a/tree.go b/tree.go
index 4f1da271..a39f43bf 100644
--- a/tree.go
+++ b/tree.go
@@ -1,10 +1,11 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
-// in the LICENSE file.
+// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
package gin
import (
+ "net/url"
"strings"
"unicode"
)
@@ -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
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
-func (n *node) getValue(path string, po Params) (handlers HandlersChain, p Params, tsr bool) {
+func (n *node) getValue(path string, po Params, unescape bool) (handlers HandlersChain, p Params, tsr bool) {
p = po
walk: // Outer loop for walking the tree
for {
@@ -406,7 +407,15 @@ walk: // Outer loop for walking the tree
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[1:]
- p[i].Value = path[:end]
+ val := path[:end]
+ if unescape {
+ var err error
+ if p[i].Value, err = url.QueryUnescape(val); err != nil {
+ p[i].Value = val // fallback, in case of error
+ }
+ } else {
+ p[i].Value = val
+ }
// we need to go deeper!
if end < len(path) {
@@ -423,7 +432,8 @@ walk: // Outer loop for walking the tree
if handlers = n.handlers; handlers != nil {
return
- } else if len(n.children) == 1 {
+ }
+ if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
@@ -440,7 +450,14 @@ walk: // Outer loop for walking the tree
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
p[i].Key = n.path[2:]
- p[i].Value = path
+ if unescape {
+ var err error
+ if p[i].Value, err = url.QueryUnescape(path); err != nil {
+ p[i].Value = path // fallback, in case of error
+ }
+ } else {
+ p[i].Value = path
+ }
handlers = n.handlers
return
diff --git a/tree_test.go b/tree_test.go
index ed21783c..c0edd42b 100644
--- a/tree_test.go
+++ b/tree_test.go
@@ -1,6 +1,6 @@
// Copyright 2013 Julien Schmidt. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be found
-// in the LICENSE file.
+// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
package gin
@@ -37,9 +37,14 @@ type testRequests []struct {
ps Params
}
-func checkRequests(t *testing.T, tree *node, requests testRequests) {
+func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
+ unescape := false
+ if len(unescapes) >= 1 {
+ unescape = unescapes[0]
+ }
+
for _, request := range requests {
- handler, ps, _ := tree.getValue(request.path, nil)
+ handler, ps, _ := tree.getValue(request.path, nil, unescape)
if handler == nil {
if !request.nilHandler {
@@ -197,6 +202,45 @@ func TestTreeWildcard(t *testing.T) {
checkMaxParams(t, tree)
}
+func TestUnescapeParameters(t *testing.T) {
+ tree := &node{}
+
+ routes := [...]string{
+ "/",
+ "/cmd/:tool/:sub",
+ "/cmd/:tool/",
+ "/src/*filepath",
+ "/search/:query",
+ "/files/:dir/*filepath",
+ "/info/:user/project/:project",
+ "/info/:user",
+ }
+ for _, route := range routes {
+ tree.addRoute(route, fakeHandler(route))
+ }
+
+ //printChildren(tree, "")
+ unescape := true
+ checkRequests(t, tree, testRequests{
+ {"/", false, "/", nil},
+ {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
+ {"/cmd/test", true, "", Params{Param{"tool", "test"}}},
+ {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
+ {"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}},
+ {"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}},
+ {"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}},
+ {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}},
+ {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
+ {"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}},
+ {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}},
+ {"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}},
+ {"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
+ }, unescape)
+
+ checkPriorities(t, tree)
+ checkMaxParams(t, tree)
+}
+
func catchPanic(testFunc func()) (recv interface{}) {
defer func() {
recv = recover()
@@ -430,7 +474,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/doc/",
}
for _, route := range tsrRoutes {
- handler, _, tsr := tree.getValue(route, nil)
+ handler, _, tsr := tree.getValue(route, nil, false)
if handler != nil {
t.Fatalf("non-nil handler for TSR route '%s", route)
} else if !tsr {
@@ -447,7 +491,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
"/api/world/abc",
}
for _, route := range noTsrRoutes {
- handler, _, tsr := tree.getValue(route, nil)
+ handler, _, tsr := tree.getValue(route, nil, false)
if handler != nil {
t.Fatalf("non-nil handler for No-TSR route '%s", route)
} else if tsr {
@@ -466,7 +510,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
t.Fatalf("panic inserting test route: %v", recv)
}
- handler, _, tsr := tree.getValue("/", nil)
+ handler, _, tsr := tree.getValue("/", nil, false)
if handler != nil {
t.Fatalf("non-nil handler")
} else if tsr {
@@ -617,7 +661,7 @@ func TestTreeInvalidNodeType(t *testing.T) {
// normal lookup
recv := catchPanic(func() {
- tree.getValue("/test", nil)
+ tree.getValue("/test", nil, false)
})
if rs, ok := recv.(string); !ok || rs != panicMsg {
t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
diff --git a/vendor/vendor.json b/vendor/vendor.json
new file mode 100644
index 00000000..e520540b
--- /dev/null
+++ b/vendor/vendor.json
@@ -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"
+}