Merge branch 'develop' into fix/logger

This commit is contained in:
cssivision 2017-02-28 12:08:20 +08:00
commit 0c7ed7702a
33 changed files with 1012 additions and 209 deletions

View File

@ -1,12 +1,18 @@
language: go language: go
sudo: false sudo: false
go: go:
- 1.4 - 1.6.x
- 1.5.4 - 1.7.x
- 1.6.4 - 1.8.x
- 1.7.4
- tip - tip
git:
depth: 3
install:
- go get -v github.com/kardianos/govendor
- govendor sync
script: script:
- go test -v -covermode=count -coverprofile=coverage.out - go test -v -covermode=count -coverprofile=coverage.out

36
Godeps/Godeps.json generated
View File

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

150
README.md
View File

@ -102,12 +102,23 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648
import "net/http" import "net/http"
``` ```
4. (Optional) Use latest changes (note: they may be broken and/or unstable):
```sh
$ GIN_PATH=$GOPATH/src/gopkg.in/gin-gonic/gin.v1
$ git -C $GIN_PATH checkout develop
$ git -C $GIN_PATH pull origin develop
```
## API Examples ## API Examples
#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS ### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
```go ```go
func main() { func main() {
// Disable Console Color
// gin.DisableConsoleColor()
// Creates a gin router with default middleware: // Creates a gin router with default middleware:
// logger and recovery (crash-free) middleware // logger and recovery (crash-free) middleware
router := gin.Default() router := gin.Default()
@ -127,7 +138,7 @@ func main() {
} }
``` ```
#### Parameters in path ### Parameters in path
```go ```go
func main() { func main() {
@ -152,7 +163,7 @@ func main() {
} }
``` ```
#### Querystring parameters ### Querystring parameters
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
@ -219,34 +230,66 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great id: 1234; page: 1; name: manu; message: this_is_great
``` ```
### Another example: upload file ### Upload files
References issue [#548](https://github.com/gin-gonic/gin/issues/548). #### Single file
References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
// single file
file, _ := c.FormFile("file")
log.Println(file.Filename)
file, header , err := c.Request.FormFile("upload") c.String(http.StatusOK, fmt.Printf("'%s' uploaded!", file.Filename))
filename := header.Filename
fmt.Println(header.Filename)
out, err := os.Create("./tmp/"+filename+".png")
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
}) })
router.Run(":8080") router.Run(":8080")
} }
``` ```
#### Grouping routes How to `curl`:
```bash
curl -X POST http://localhost:8080/upload \
-F "file=@/Users/appleboy/test.zip" \
-H "Content-Type: multipart/form-data"
```
#### Multiple files
See the detail [example code](examples/upload-file/multiple).
```go
func main() {
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["upload[]"]
for _, file := range files {
log.Println(file.Filename)
}
c.String(http.StatusOK, fmt.Printf("%d files uploaded!", len(files)))
})
router.Run(":8080")
}
```
How to `curl`:
```bash
curl -X POST http://localhost:8080/upload \
-F "upload[]=@/Users/appleboy/test1.zip" \
-F "upload[]=@/Users/appleboy/test2.zip" \
-H "Content-Type: multipart/form-data"
```
### Grouping routes
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
@ -272,7 +315,7 @@ func main() {
``` ```
#### Blank Gin without middleware by default ### Blank Gin without middleware by default
Use Use
@ -286,7 +329,7 @@ r := gin.Default()
``` ```
#### Using middleware ### Using middleware
```go ```go
func main() { func main() {
// Creates a router without any middleware by default // Creates a router without any middleware by default
@ -321,7 +364,7 @@ func main() {
} }
``` ```
#### Model binding and validation ### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz). To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
@ -371,8 +414,44 @@ func main() {
} }
``` ```
### Bind Query String
See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292).
```go
package main
import "log"
import "github.com/gin-gonic/gin"
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
func main() {
route := gin.Default()
route.GET("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/develop/binding/binding.go#L45
if c.Bind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
}
c.String(200, "Success")
}
```
### Multipart/Urlencoded binding
###Multipart/Urlencoded binding
```go ```go
package main package main
@ -411,7 +490,7 @@ $ curl -v --form user=user --form password=password http://localhost:8080/login
``` ```
#### XML, JSON and YAML rendering ### XML, JSON and YAML rendering
```go ```go
func main() { func main() {
@ -450,7 +529,7 @@ func main() {
} }
``` ```
####Serving static files ### Serving static files
```go ```go
func main() { func main() {
@ -464,9 +543,9 @@ func main() {
} }
``` ```
####HTML rendering ### HTML rendering
Using LoadHTMLTemplates() Using LoadHTMLGlob() or LoadHTMLFiles()
```go ```go
func main() { func main() {
@ -543,8 +622,11 @@ func main() {
} }
``` ```
### Multitemplate
#### Redirects Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
### Redirects
Issuing a HTTP redirect is easy: Issuing a HTTP redirect is easy:
@ -556,7 +638,7 @@ r.GET("/test", func(c *gin.Context) {
Both internal and external locations are supported. Both internal and external locations are supported.
#### Custom Middleware ### Custom Middleware
```go ```go
func Logger() gin.HandlerFunc { func Logger() gin.HandlerFunc {
@ -596,7 +678,7 @@ func main() {
} }
``` ```
#### Using BasicAuth() middleware ### Using BasicAuth() middleware
```go ```go
// simulate some private data // simulate some private data
var secrets = gin.H{ var secrets = gin.H{
@ -635,7 +717,7 @@ func main() {
``` ```
#### Goroutines inside a middleware ### Goroutines inside a middleware
When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy.
```go ```go
@ -667,7 +749,7 @@ func main() {
} }
``` ```
#### Custom HTTP configuration ### Custom HTTP configuration
Use `http.ListenAndServe()` directly, like this: Use `http.ListenAndServe()` directly, like this:
@ -694,7 +776,7 @@ func main() {
} }
``` ```
#### Graceful restart or stop ### Graceful restart or stop
Do you want to graceful restart or stop your web server? Do you want to graceful restart or stop your web server?
There are some ways this can be done. There are some ways this can be done.
@ -725,7 +807,7 @@ An alternative to endless:
- You should add/modify tests to cover your proposed code changes. - You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README. - If your pull request contains a new feature, please document it on the README.
## Example ## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"reflect" "reflect"
"strconv" "strconv"
"time"
) )
func mapForm(ptr interface{}, form map[string][]string) error { func mapForm(ptr interface{}, form map[string][]string) error {
@ -52,6 +53,12 @@ func mapForm(ptr interface{}, form map[string][]string) error {
} }
val.Field(i).Set(slice) val.Field(i).Set(slice)
} else { } else {
if _, isTime := structField.Interface().(time.Time); isTime {
if err := setTimeField(inputValue[0], typeField, structField); err != nil {
return err
}
continue
}
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err return err
} }
@ -140,6 +147,26 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
return err return err
} }
func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
timeFormat := structField.Tag.Get("time_format")
if timeFormat == "" {
return errors.New("Blank time format")
}
l := time.Local
if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
l = time.UTC
}
t, err := time.ParseInLocation(timeFormat, val, l)
if err != nil {
return err
}
value.Set(reflect.ValueOf(t))
return nil
}
// Don't pass in pointers to bind to. Can lead to bugs. See: // Don't pass in pointers to bind to. Can lead to bugs. See:
// https://github.com/codegangsta/martini-contrib/issues/40 // https://github.com/codegangsta/martini-contrib/issues/40
// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 // https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659

28
binding/msgpack.go Normal file
View File

@ -0,0 +1,28 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"net/http"
"github.com/ugorji/go/codec"
)
type msgpackBinding struct{}
func (msgpackBinding) Name() string {
return "msgpack"
}
func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
if err := codec.NewDecoder(req.Body, new(codec.MsgpackHandle)).Decode(&obj); err != nil {
//var decoder *codec.Decoder = codec.NewDecoder(req.Body, &codec.MsgpackHandle)
//if err := decoder.Decode(&obj); err != nil {
return err
}
return validate(obj)
}

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"io" "io"
"math" "math"
"mime/multipart"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -16,8 +17,7 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
"github.com/manucorporat/sse" "gopkg.in/gin-contrib/sse.v0"
"golang.org/x/net/context"
) )
// Content-Type MIME of the most common data formats // Content-Type MIME of the most common data formats
@ -31,7 +31,10 @@ const (
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
) )
const abortIndex int8 = math.MaxInt8 / 2 const (
defaultMemory = 32 << 20 // 32 MB
abortIndex int8 = math.MaxInt8 / 2
)
// Context is the most important part of gin. It allows us to pass variables between middleware, // Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example. // manage the flow, validate the JSON of a request and render a JSON response for example.
@ -50,8 +53,6 @@ type Context struct {
Accepted []string Accepted []string
} }
var _ context.Context = &Context{}
/************************************/ /************************************/
/********** CONTEXT CREATION ********/ /********** CONTEXT CREATION ********/
/************************************/ /************************************/
@ -67,7 +68,7 @@ func (c *Context) reset() {
} }
// Copy returns a copy of the current context that can be safely used outside the request's scope. // Copy returns a copy of the current context that can be safely used outside the request's scope.
// This have to be used then the context has to be passed to a goroutine. // This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context { func (c *Context) Copy() *Context {
var cp = *c var cp = *c
cp.writermem.ResponseWriter = nil cp.writermem.ResponseWriter = nil
@ -119,6 +120,13 @@ func (c *Context) AbortWithStatus(code int) {
c.Abort() c.Abort()
} }
// AbortWithStatusJSON calls `Abort()` and then `JSON` internally. This method stops the chain, writes the status code and return a JSON body
// It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) {
c.Abort()
c.JSON(code, jsonObj)
}
// AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and // AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and
// pushes the specified error to `c.Errors`. // pushes the specified error to `c.Errors`.
// See Context.Error() for more details. // See Context.Error() for more details.
@ -166,9 +174,7 @@ func (c *Context) Set(key string, value interface{}) {
// Get returns the value for the given key, ie: (value, true). // Get returns the value for the given key, ie: (value, true).
// If the value does not exists it returns (nil, false) // If the value does not exists it returns (nil, false)
func (c *Context) Get(key string) (value interface{}, exists bool) { func (c *Context) Get(key string) (value interface{}, exists bool) {
if c.Keys != nil { value, exists = c.Keys[key]
value, exists = c.Keys[key]
}
return return
} }
@ -296,7 +302,7 @@ func (c *Context) PostFormArray(key string) []string {
func (c *Context) GetPostFormArray(key string) ([]string, bool) { func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request req := c.Request
req.ParseForm() req.ParseForm()
req.ParseMultipartForm(32 << 20) // 32 MB req.ParseMultipartForm(defaultMemory)
if values := req.PostForm[key]; len(values) > 0 { if values := req.PostForm[key]; len(values) > 0 {
return values, true return values, true
} }
@ -308,6 +314,18 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false return []string{}, false
} }
// FormFile returns the first file for the provided form key.
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
_, fh, err := c.Request.FormFile(name)
return fh, err
}
// MultipartForm is the parsed multipart form, including file uploads.
func (c *Context) MultipartForm() (*multipart.Form, error) {
err := c.Request.ParseMultipartForm(defaultMemory)
return c.Request.MultipartForm, err
}
// Bind checks the Content-Type to select a binding engine automatically, // Bind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used: // Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding // "application/json" --> JSON binding
@ -338,13 +356,10 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
// ClientIP implements a best effort algorithm to return the real client IP, it parses // ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP { if c.engine.ForwardedByClientIP {
clientIP := strings.TrimSpace(c.requestHeader("X-Real-Ip")) clientIP := c.requestHeader("X-Forwarded-For")
if len(clientIP) > 0 {
return clientIP
}
clientIP = c.requestHeader("X-Forwarded-For")
if index := strings.IndexByte(clientIP, ','); index >= 0 { if index := strings.IndexByte(clientIP, ','); index >= 0 {
clientIP = clientIP[0:index] clientIP = clientIP[0:index]
} }
@ -352,10 +367,22 @@ func (c *Context) ClientIP() string {
if len(clientIP) > 0 { if len(clientIP) > 0 {
return clientIP return clientIP
} }
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
if len(clientIP) > 0 {
return clientIP
}
} }
if c.engine.AppEngine {
if addr := c.Request.Header.Get("X-Appengine-Remote-Addr"); addr != "" {
return addr
}
}
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil { if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
return ip return ip
} }
return "" return ""
} }
@ -364,6 +391,16 @@ func (c *Context) ContentType() string {
return filterFlags(c.requestHeader("Content-Type")) return filterFlags(c.requestHeader("Content-Type"))
} }
// IsWebsocket returns true if the request headers indicate that a websocket
// handshake is being initiated by the client.
func (c *Context) IsWebsocket() bool {
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
return true
}
return false
}
func (c *Context) requestHeader(key string) string { func (c *Context) requestHeader(key string) string {
if values, _ := c.Request.Header[key]; len(values) > 0 { if values, _ := c.Request.Header[key]; len(values) > 0 {
return values[0] return values[0]
@ -375,6 +412,19 @@ func (c *Context) requestHeader(key string) string {
/******** RESPONSE RENDERING ********/ /******** RESPONSE RENDERING ********/
/************************************/ /************************************/
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function
func bodyAllowedForStatus(status int) bool {
switch {
case status >= 100 && status <= 199:
return false
case status == 204:
return false
case status == 304:
return false
}
return true
}
func (c *Context) Status(code int) { func (c *Context) Status(code int) {
c.writermem.WriteHeader(code) c.writermem.WriteHeader(code)
} }
@ -424,6 +474,13 @@ func (c *Context) Cookie(name string) (string, error) {
func (c *Context) Render(code int, r render.Render) { func (c *Context) Render(code int, r render.Render) {
c.Status(code) c.Status(code)
if !bodyAllowedForStatus(code) {
r.WriteContentType(c.Writer)
c.Writer.WriteHeaderNow()
return
}
if err := r.Render(c.Writer); err != nil { if err := r.Render(c.Writer); err != nil {
panic(err) panic(err)
} }
@ -448,10 +505,7 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
// JSON serializes the given struct as JSON into the response body. // JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) { func (c *Context) JSON(code int, obj interface{}) {
c.Status(code) c.Render(code, render.JSON{Data: obj})
if err := render.WriteJSON(c.Writer, obj); err != nil {
panic(err)
}
} }
// XML serializes the given struct as XML into the response body. // XML serializes the given struct as XML into the response body.
@ -467,8 +521,7 @@ func (c *Context) YAML(code int, obj interface{}) {
// String writes the given string into the response body. // String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) { func (c *Context) String(code int, format string, values ...interface{}) {
c.Status(code) c.Render(code, render.String{Format: format, Data: values})
render.WriteString(c.Writer, format, values)
} }
// Redirect returns a HTTP redirect to the specific location. // Redirect returns a HTTP redirect to the specific location.

7
context_appengine.go Normal file
View File

@ -0,0 +1,7 @@
// +build appengine
package gin
func init() {
defaultAppEngine = true
}

View File

@ -7,6 +7,7 @@ package gin
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"html/template" "html/template"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
@ -15,10 +16,13 @@ import (
"testing" "testing"
"time" "time"
"github.com/manucorporat/sse"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"gopkg.in/gin-contrib/sse.v0"
) )
var _ context.Context = &Context{}
// Unit tests TODO // Unit tests TODO
// func (c *Context) File(filepath string) { // func (c *Context) File(filepath string) {
// func (c *Context) Negotiate(code int, config Negotiate) { // func (c *Context) Negotiate(code int, config Negotiate) {
@ -38,6 +42,8 @@ func createMultipartRequest() *http.Request {
must(mw.WriteField("array", "first")) must(mw.WriteField("array", "first"))
must(mw.WriteField("array", "second")) must(mw.WriteField("array", "second"))
must(mw.WriteField("id", "")) must(mw.WriteField("id", ""))
must(mw.WriteField("time_local", "31/12/2016 14:55"))
must(mw.WriteField("time_utc", "31/12/2016 14:55"))
req, err := http.NewRequest("POST", "/", body) req, err := http.NewRequest("POST", "/", body)
must(err) must(err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
@ -50,6 +56,37 @@ func must(err error) {
} }
} }
func TestContextFormFile(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) {
w.Write([]byte("test"))
}
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", buf)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
f, err := c.FormFile("file")
if assert.NoError(t, err) {
assert.Equal(t, "test", f.Filename)
}
}
func TestContextMultipartForm(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.WriteField("foo", "bar")
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", buf)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
f, err := c.MultipartForm()
if assert.NoError(t, err) {
assert.NotNil(t, f)
}
}
func TestContextReset(t *testing.T) { func TestContextReset(t *testing.T) {
router := New() router := New()
c := router.allocateContext() c := router.allocateContext()
@ -74,7 +111,7 @@ func TestContextReset(t *testing.T) {
} }
func TestContextHandlers(t *testing.T) { func TestContextHandlers(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
assert.Nil(t, c.handlers) assert.Nil(t, c.handlers)
assert.Nil(t, c.handlers.Last()) assert.Nil(t, c.handlers.Last())
@ -95,7 +132,7 @@ func TestContextHandlers(t *testing.T) {
// TestContextSetGet tests that a parameter is set correctly on the // TestContextSetGet tests that a parameter is set correctly on the
// current context and can be retrieved using Get. // current context and can be retrieved using Get.
func TestContextSetGet(t *testing.T) { func TestContextSetGet(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("foo", "bar") c.Set("foo", "bar")
value, err := c.Get("foo") value, err := c.Get("foo")
@ -111,7 +148,7 @@ func TestContextSetGet(t *testing.T) {
} }
func TestContextSetGetValues(t *testing.T) { func TestContextSetGetValues(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("string", "this is a string") c.Set("string", "this is a string")
c.Set("int32", int32(-42)) c.Set("int32", int32(-42))
c.Set("int64", int64(42424242424242)) c.Set("int64", int64(42424242424242))
@ -132,7 +169,7 @@ func TestContextSetGetValues(t *testing.T) {
} }
func TestContextCopy(t *testing.T) { func TestContextCopy(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.index = 2 c.index = 2
c.Request, _ = http.NewRequest("POST", "/hola", nil) c.Request, _ = http.NewRequest("POST", "/hola", nil)
c.handlers = HandlersChain{func(c *Context) {}} c.handlers = HandlersChain{func(c *Context) {}}
@ -151,7 +188,7 @@ func TestContextCopy(t *testing.T) {
} }
func TestContextHandlerName(t *testing.T) { func TestContextHandlerName(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest} c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName()) assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName())
@ -162,7 +199,7 @@ func handlerNameTest(c *Context) {
} }
func TestContextQuery(t *testing.T) { func TestContextQuery(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil) c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil)
value, ok := c.GetQuery("foo") value, ok := c.GetQuery("foo")
@ -197,7 +234,7 @@ func TestContextQuery(t *testing.T) {
} }
func TestContextQueryAndPostForm(t *testing.T) { func TestContextQueryAndPostForm(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second") body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body) c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body)
c.Request.Header.Add("Content-Type", MIMEPOSTForm) c.Request.Header.Add("Content-Type", MIMEPOSTForm)
@ -270,15 +307,18 @@ func TestContextQueryAndPostForm(t *testing.T) {
} }
func TestContextPostFormMultipart(t *testing.T) { func TestContextPostFormMultipart(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request = createMultipartRequest() c.Request = createMultipartRequest()
var obj struct { var obj struct {
Foo string `form:"foo"` Foo string `form:"foo"`
Bar string `form:"bar"` Bar string `form:"bar"`
BarAsInt int `form:"bar"` BarAsInt int `form:"bar"`
Array []string `form:"array"` Array []string `form:"array"`
ID string `form:"id"` ID string `form:"id"`
TimeLocal time.Time `form:"time_local" time_format:"02/01/2006 15:04"`
TimeUTC time.Time `form:"time_utc" time_format:"02/01/2006 15:04" time_utc:"1"`
BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"`
} }
assert.NoError(t, c.Bind(&obj)) assert.NoError(t, c.Bind(&obj))
assert.Equal(t, obj.Foo, "bar") assert.Equal(t, obj.Foo, "bar")
@ -286,6 +326,11 @@ func TestContextPostFormMultipart(t *testing.T) {
assert.Equal(t, obj.BarAsInt, 10) assert.Equal(t, obj.BarAsInt, 10)
assert.Equal(t, obj.Array, []string{"first", "second"}) assert.Equal(t, obj.Array, []string{"first", "second"})
assert.Equal(t, obj.ID, "") assert.Equal(t, obj.ID, "")
assert.Equal(t, obj.TimeLocal.Format("02/01/2006 15:04"), "31/12/2016 14:55")
assert.Equal(t, obj.TimeLocal.Location(), time.Local)
assert.Equal(t, obj.TimeUTC.Format("02/01/2006 15:04"), "31/12/2016 14:55")
assert.Equal(t, obj.TimeUTC.Location(), time.UTC)
assert.True(t, obj.BlankTime.IsZero())
value, ok := c.GetQuery("foo") value, ok := c.GetQuery("foo")
assert.False(t, ok) assert.False(t, ok)
@ -334,46 +379,106 @@ func TestContextPostFormMultipart(t *testing.T) {
} }
func TestContextSetCookie(t *testing.T) { func TestContextSetCookie(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.SetCookie("user", "gin", 1, "/", "localhost", true, true) c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
} }
func TestContextGetCookie(t *testing.T) { func TestContextGetCookie(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request, _ = http.NewRequest("GET", "/get", nil)
c.Request.Header.Set("Cookie", "user=gin") c.Request.Header.Set("Cookie", "user=gin")
cookie, _ := c.Cookie("user") cookie, _ := c.Cookie("user")
assert.Equal(t, cookie, "gin") assert.Equal(t, cookie, "gin")
} }
func TestContextBodyAllowedForStatus(t *testing.T) {
assert.Equal(t, false, bodyAllowedForStatus(102))
assert.Equal(t, false, bodyAllowedForStatus(204))
assert.Equal(t, false, bodyAllowedForStatus(304))
assert.Equal(t, true, bodyAllowedForStatus(500))
}
type TestPanicRender struct {
}
func (*TestPanicRender) Render(http.ResponseWriter) error {
return errors.New("TestPanicRender")
}
func (*TestPanicRender) WriteContentType(http.ResponseWriter) {}
func TestContextRenderPanicIfErr(t *testing.T) {
defer func() {
r := recover()
assert.Equal(t, "TestPanicRender", fmt.Sprint(r))
}()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Render(http.StatusOK, &TestPanicRender{})
assert.Fail(t, "Panic not detected")
}
// Tests that the response is serialized as JSON // Tests that the response is serialized as JSON
// and Content-Type is set to application/json // and Content-Type is set to application/json
func TestContextRenderJSON(t *testing.T) { func TestContextRenderJSON(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.JSON(201, H{"foo": "bar"}) c.JSON(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
// Tests that no JSON is rendered if code is 204
func TestContextRenderNoContentJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.JSON(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// Tests that the response is serialized as JSON // Tests that the response is serialized as JSON
// we change the content-type before // we change the content-type before
func TestContextRenderAPIJSON(t *testing.T) { func TestContextRenderAPIJSON(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "application/vnd.api+json") c.Header("Content-Type", "application/vnd.api+json")
c.JSON(201, H{"foo": "bar"}) c.JSON(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, 201, w.Code)
assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type"))
}
// Tests that no Custom JSON is rendered if code is 204
func TestContextRenderNoContentAPIJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "application/vnd.api+json")
c.JSON(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json")
} }
// Tests that the response is serialized as JSON // Tests that the response is serialized as JSON
// and Content-Type is set to application/json // and Content-Type is set to application/json
func TestContextRenderIndentedJSON(t *testing.T) { func TestContextRenderIndentedJSON(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
assert.Equal(t, w.Code, 201) assert.Equal(t, w.Code, 201)
@ -381,10 +486,23 @@ func TestContextRenderIndentedJSON(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
} }
// Tests that no Custom JSON is rendered if code is 204
func TestContextRenderNoContentIndentedJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
}
// Tests that the response executes the templates // Tests that the response executes the templates
// and responds with Content-Type set to text/html // and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) { func TestContextRenderHTML(t *testing.T) {
c, w, router := CreateTestContext() w := httptest.NewRecorder()
c, router := CreateTestContext(w)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ) router.SetHTMLTemplate(templ)
@ -395,10 +513,26 @@ func TestContextRenderHTML(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
} }
// Tests that no HTML is rendered if code is 204
func TestContextRenderNoContentHTML(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
c.HTML(204, "t", H{"name": "alexandernyquist"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
// TestContextXML tests that the response is serialized as XML // TestContextXML tests that the response is serialized as XML
// and Content-Type is set to application/xml // and Content-Type is set to application/xml
func TestContextRenderXML(t *testing.T) { func TestContextRenderXML(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.XML(201, H{"foo": "bar"}) c.XML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, w.Code, 201)
@ -406,10 +540,24 @@ func TestContextRenderXML(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
} }
// Tests that no XML is rendered if code is 204
func TestContextRenderNoContentXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.XML(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
}
// TestContextString tests that the response is returned // TestContextString tests that the response is returned
// with Content-Type set to text/plain // with Content-Type set to text/plain
func TestContextRenderString(t *testing.T) { func TestContextRenderString(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.String(201, "test %s %d", "string", 2) c.String(201, "test %s %d", "string", 2)
assert.Equal(t, w.Code, 201) assert.Equal(t, w.Code, 201)
@ -417,10 +565,24 @@ func TestContextRenderString(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
} }
// Tests that no String is rendered if code is 204
func TestContextRenderNoContentString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.String(204, "test %s %d", "string", 2)
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
}
// TestContextString tests that the response is returned // TestContextString tests that the response is returned
// with Content-Type set to text/html // with Content-Type set to text/html
func TestContextRenderHTMLString(t *testing.T) { func TestContextRenderHTMLString(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8") c.Header("Content-Type", "text/html; charset=utf-8")
c.String(201, "<html>%s %d</html>", "string", 3) c.String(201, "<html>%s %d</html>", "string", 3)
@ -429,10 +591,25 @@ func TestContextRenderHTMLString(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
} }
// Tests that no HTML String is rendered if code is 204
func TestContextRenderNoContentHTMLString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(204, "<html>%s %d</html>", "string", 3)
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
// TestContextData tests that the response can be written from `bytesting` // TestContextData tests that the response can be written from `bytesting`
// with specified MIME type // with specified MIME type
func TestContextRenderData(t *testing.T) { func TestContextRenderData(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Data(201, "text/csv", []byte(`foo,bar`)) c.Data(201, "text/csv", []byte(`foo,bar`))
assert.Equal(t, w.Code, 201) assert.Equal(t, w.Code, 201)
@ -440,8 +617,22 @@ func TestContextRenderData(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
} }
// Tests that no Custom Data is rendered if code is 204
func TestContextRenderNoContentData(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Data(204, "text/csv", []byte(`foo,bar`))
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
}
func TestContextRenderSSE(t *testing.T) { func TestContextRenderSSE(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.SSEvent("float", 1.5) c.SSEvent("float", 1.5)
c.Render(-1, sse.Event{ c.Render(-1, sse.Event{
Id: "123", Id: "123",
@ -456,7 +647,9 @@ func TestContextRenderSSE(t *testing.T) {
} }
func TestContextRenderFile(t *testing.T) { func TestContextRenderFile(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil) c.Request, _ = http.NewRequest("GET", "/", nil)
c.File("./gin.go") c.File("./gin.go")
@ -468,7 +661,9 @@ func TestContextRenderFile(t *testing.T) {
// TestContextRenderYAML tests that the response is serialized as YAML // TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/x-yaml // and Content-Type is set to application/x-yaml
func TestContextRenderYAML(t *testing.T) { func TestContextRenderYAML(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.YAML(201, H{"foo": "bar"}) c.YAML(201, H{"foo": "bar"})
assert.Equal(t, w.Code, 201) assert.Equal(t, w.Code, 201)
@ -477,7 +672,7 @@ func TestContextRenderYAML(t *testing.T) {
} }
func TestContextHeaders(t *testing.T) { func TestContextHeaders(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Header("Content-Type", "text/plain") c.Header("Content-Type", "text/plain")
c.Header("X-Custom", "value") c.Header("X-Custom", "value")
@ -494,7 +689,9 @@ func TestContextHeaders(t *testing.T) {
// TODO // TODO
func TestContextRenderRedirectWithRelativePath(t *testing.T) { func TestContextRenderRedirectWithRelativePath(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
assert.Panics(t, func() { c.Redirect(299, "/new_path") }) assert.Panics(t, func() { c.Redirect(299, "/new_path") })
assert.Panics(t, func() { c.Redirect(309, "/new_path") }) assert.Panics(t, func() { c.Redirect(309, "/new_path") })
@ -506,7 +703,9 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) {
} }
func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Redirect(302, "http://google.com") c.Redirect(302, "http://google.com")
c.Writer.WriteHeaderNow() c.Writer.WriteHeaderNow()
@ -516,7 +715,9 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
} }
func TestContextRenderRedirectWith201(t *testing.T) { func TestContextRenderRedirectWith201(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Redirect(201, "/resource") c.Redirect(201, "/resource")
c.Writer.WriteHeaderNow() c.Writer.WriteHeaderNow()
@ -526,7 +727,7 @@ func TestContextRenderRedirectWith201(t *testing.T) {
} }
func TestContextRenderRedirectAll(t *testing.T) { func TestContextRenderRedirectAll(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
assert.Panics(t, func() { c.Redirect(200, "/resource") }) assert.Panics(t, func() { c.Redirect(200, "/resource") })
assert.Panics(t, func() { c.Redirect(202, "/resource") }) assert.Panics(t, func() { c.Redirect(202, "/resource") })
@ -537,7 +738,7 @@ func TestContextRenderRedirectAll(t *testing.T) {
} }
func TestContextNegotiationFormat(t *testing.T) { func TestContextNegotiationFormat(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "", nil) c.Request, _ = http.NewRequest("POST", "", nil)
assert.Panics(t, func() { c.NegotiateFormat() }) assert.Panics(t, func() { c.NegotiateFormat() })
@ -546,7 +747,7 @@ func TestContextNegotiationFormat(t *testing.T) {
} }
func TestContextNegotiationFormatWithAccept(t *testing.T) { func TestContextNegotiationFormatWithAccept(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
@ -556,7 +757,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) {
} }
func TestContextNegotiationFormatCustum(t *testing.T) { func TestContextNegotiationFormatCustum(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
@ -569,7 +770,7 @@ func TestContextNegotiationFormatCustum(t *testing.T) {
} }
func TestContextIsAborted(t *testing.T) { func TestContextIsAborted(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
assert.False(t, c.IsAborted()) assert.False(t, c.IsAborted())
c.Abort() c.Abort()
@ -585,7 +786,9 @@ func TestContextIsAborted(t *testing.T) {
// TestContextData tests that the response can be written from `bytesting` // TestContextData tests that the response can be written from `bytesting`
// with specified MIME type // with specified MIME type
func TestContextAbortWithStatus(t *testing.T) { func TestContextAbortWithStatus(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.index = 4 c.index = 4
c.AbortWithStatus(401) c.AbortWithStatus(401)
@ -595,8 +798,38 @@ func TestContextAbortWithStatus(t *testing.T) {
assert.True(t, c.IsAborted()) assert.True(t, c.IsAborted())
} }
type testJSONAbortMsg struct {
Foo string `json:"foo"`
Bar string `json:"bar"`
}
func TestContextAbortWithStatusJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.index = 4
in := new(testJSONAbortMsg)
in.Bar = "barValue"
in.Foo = "fooValue"
c.AbortWithStatusJSON(415, in)
assert.Equal(t, c.index, abortIndex)
assert.Equal(t, c.Writer.Status(), 415)
assert.Equal(t, w.Code, 415)
assert.True(t, c.IsAborted())
contentType := w.Header().Get("Content-Type")
assert.Equal(t, contentType, "application/json; charset=utf-8")
buf := new(bytes.Buffer)
buf.ReadFrom(w.Body)
jsonStringBody := buf.String()
assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
}
func TestContextError(t *testing.T) { func TestContextError(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
assert.Empty(t, c.Errors) assert.Empty(t, c.Errors)
c.Error(errors.New("first error")) c.Error(errors.New("first error"))
@ -622,7 +855,7 @@ func TestContextError(t *testing.T) {
} }
func TestContextTypedError(t *testing.T) { func TestContextTypedError(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
@ -636,7 +869,9 @@ func TestContextTypedError(t *testing.T) {
} }
func TestContextAbortWithError(t *testing.T) { func TestContextAbortWithError(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.AbortWithError(401, errors.New("bad input")).SetMeta("some input") c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
assert.Equal(t, w.Code, 401) assert.Equal(t, w.Code, 401)
@ -645,27 +880,37 @@ func TestContextAbortWithError(t *testing.T) {
} }
func TestContextClientIP(t *testing.T) { func TestContextClientIP(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ")
c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30")
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.RemoteAddr = " 40.40.40.40:42123 " c.Request.RemoteAddr = " 40.40.40.40:42123 "
assert.Equal(t, c.ClientIP(), "10.10.10.10") assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Real-IP")
assert.Equal(t, c.ClientIP(), "20.20.20.20")
c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ")
assert.Equal(t, c.ClientIP(), "30.30.30.30")
c.Request.Header.Del("X-Forwarded-For") c.Request.Header.Del("X-Forwarded-For")
assert.Equal(t, c.ClientIP(), "40.40.40.40") assert.Equal(t, "10.10.10.10", c.ClientIP())
c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ")
assert.Equal(t, "30.30.30.30", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For")
c.Request.Header.Del("X-Real-IP")
c.engine.AppEngine = true
assert.Equal(t, "50.50.50.50", c.ClientIP())
c.Request.Header.Del("X-Appengine-Remote-Addr")
assert.Equal(t, "40.40.40.40", c.ClientIP())
// no port
c.Request.RemoteAddr = "50.50.50.50"
assert.Equal(t, "", c.ClientIP())
} }
func TestContextContentType(t *testing.T) { func TestContextContentType(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", nil) c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
@ -673,7 +918,7 @@ func TestContextContentType(t *testing.T) {
} }
func TestContextAutoBindJSON(t *testing.T) { func TestContextAutoBindJSON(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON) c.Request.Header.Add("Content-Type", MIMEJSON)
@ -688,7 +933,9 @@ func TestContextAutoBindJSON(t *testing.T) {
} }
func TestContextBindWithJSON(t *testing.T) { func TestContextBindWithJSON(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
@ -703,7 +950,9 @@ func TestContextBindWithJSON(t *testing.T) {
} }
func TestContextBadAutoBind(t *testing.T) { func TestContextBadAutoBind(t *testing.T) {
c, w, _ := CreateTestContext() w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON) c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct { var obj struct {
@ -722,7 +971,7 @@ func TestContextBadAutoBind(t *testing.T) {
} }
func TestContextGolangContext(t *testing.T) { func TestContextGolangContext(t *testing.T) {
c, _, _ := CreateTestContext() c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
assert.NoError(t, c.Err()) assert.NoError(t, c.Err())
assert.Nil(t, c.Done()) assert.Nil(t, c.Done())
@ -736,3 +985,25 @@ func TestContextGolangContext(t *testing.T) {
assert.Equal(t, c.Value("foo"), "bar") assert.Equal(t, c.Value("foo"), "bar")
assert.Nil(t, c.Value(1)) assert.Nil(t, c.Value(1))
} }
func TestWebsocketsRequired(t *testing.T) {
// Example request from spec: https://tools.ietf.org/html/rfc6455#section-1.2
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/chat", nil)
c.Request.Header.Set("Host", "server.example.com")
c.Request.Header.Set("Upgrade", "websocket")
c.Request.Header.Set("Connection", "Upgrade")
c.Request.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
c.Request.Header.Set("Origin", "http://example.com")
c.Request.Header.Set("Sec-WebSocket-Protocol", "chat, superchat")
c.Request.Header.Set("Sec-WebSocket-Version", "13")
assert.True(t, c.IsWebsocket())
// Normal request, no websocket required.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/chat", nil)
c.Request.Header.Set("Host", "server.example.com")
assert.False(t, c.IsWebsocket())
}

View File

@ -72,7 +72,7 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
} }
// Implements the error interface // Implements the error interface
func (msg *Error) Error() string { func (msg Error) Error() string {
return msg.Err.Error() return msg.Err.Error()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

17
gin.go
View File

@ -15,10 +15,11 @@ import (
) )
// Version is Framework's version // Version is Framework's version
const Version = "v1.0rc2" const Version = "v1.1.4"
var default404Body = []byte("404 page not found") var default404Body = []byte("404 page not found")
var default405Body = []byte("405 method not allowed") var default405Body = []byte("405 method not allowed")
var defaultAppEngine bool
type HandlerFunc func(*Context) type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc type HandlersChain []HandlerFunc
@ -78,6 +79,10 @@ type (
// handler. // handler.
HandleMethodNotAllowed bool HandleMethodNotAllowed bool
ForwardedByClientIP bool ForwardedByClientIP bool
// #726 #755 If enabled, it will thrust some headers starting with
// 'X-AppEngine...' for better integration with that PaaS.
AppEngine bool
} }
) )
@ -101,6 +106,7 @@ func New() *Engine {
RedirectFixedPath: false, RedirectFixedPath: false,
HandleMethodNotAllowed: false, HandleMethodNotAllowed: false,
ForwardedByClientIP: true, ForwardedByClientIP: true,
AppEngine: defaultAppEngine,
trees: make(methodTrees, 0, 9), trees: make(methodTrees, 0, 9),
} }
engine.RouterGroup.engine = engine engine.RouterGroup.engine = engine
@ -267,6 +273,15 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.pool.Put(c) engine.pool.Put(c)
} }
// Re-enter a context that has been rewritten.
// This can be done by setting c.Request.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
func (engine *Engine) handleHTTPRequest(context *Context) { func (engine *Engine) handleHTTPRequest(context *Context) {
httpMethod := context.Request.Method httpMethod := context.Request.Method
path := context.Request.URL.Path path := context.Request.URL.Path

View File

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

View File

@ -14,16 +14,21 @@ import (
) )
var ( var (
green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
reset = string([]byte{27, 91, 48, 109}) reset = string([]byte{27, 91, 48, 109})
disableColor = false
) )
func DisableConsoleColor() {
disableColor = true
}
func ErrorLogger() HandlerFunc { func ErrorLogger() HandlerFunc {
return ErrorLoggerT(ErrorTypeAny) return ErrorLoggerT(ErrorTypeAny)
} }
@ -49,7 +54,7 @@ func Logger() HandlerFunc {
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
isTerm := true isTerm := true
if w, ok := out.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) { if w, ok := out.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) || disableColor {
isTerm = false isTerm = false
} }
@ -87,12 +92,12 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
} }
comment := c.Errors.ByType(ErrorTypePrivate).String() comment := c.Errors.ByType(ErrorTypePrivate).String()
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s", fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %s %-7s %s\n%s",
end.Format("2006/01/02 - 15:04:05"), end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, reset, statusColor, statusCode, reset,
latency, latency,
clientIP, clientIP,
methodColor, reset, method, methodColor, method, reset,
path, path,
comment, comment,
) )

View File

@ -113,16 +113,16 @@ func TestErrorLogger(t *testing.T) {
}) })
w := performRequest(router, "GET", "/error") w := performRequest(router, "GET", "/error")
assert.Equal(t, w.Code, 200) assert.Equal(t, 200, w.Code)
assert.Equal(t, w.Body.String(), "{\"error\":\"this is an error\"}\n") assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
w = performRequest(router, "GET", "/abort") w = performRequest(router, "GET", "/abort")
assert.Equal(t, w.Code, 401) assert.Equal(t, 401, w.Code)
assert.Equal(t, w.Body.String(), "{\"error\":\"no authorized\"}\n") assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
w = performRequest(router, "GET", "/print") w = performRequest(router, "GET", "/print")
assert.Equal(t, w.Code, 500) assert.Equal(t, 500, w.Code)
assert.Equal(t, w.Body.String(), "hola!{\"error\":\"this is an error\"}\n") assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
} }
func TestSkippingPaths(t *testing.T) { func TestSkippingPaths(t *testing.T) {
@ -138,3 +138,10 @@ func TestSkippingPaths(t *testing.T) {
performRequest(router, "GET", "/skipped") performRequest(router, "GET", "/skipped")
assert.Contains(t, buffer.String(), "") assert.Contains(t, buffer.String(), "")
} }
func TestDisableConsoleColor(t *testing.T) {
New()
assert.False(t, disableColor)
DisableConsoleColor()
assert.True(t, disableColor)
}

View File

@ -10,8 +10,8 @@ import (
"testing" "testing"
"github.com/manucorporat/sse"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/gin-contrib/sse.v0"
) )
func TestMiddlewareGeneralCase(t *testing.T) { func TestMiddlewareGeneralCase(t *testing.T) {
@ -245,6 +245,6 @@ func TestMiddlewareWrite(t *testing.T) {
w := performRequest(router, "GET", "/") w := performRequest(router, "GET", "/")
assert.Equal(t, w.Code, 400) assert.Equal(t, 400, w.Code)
assert.Equal(t, strings.Replace(w.Body.String(), " ", "", -1), strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1)) assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
} }

View File

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

View File

@ -58,9 +58,14 @@ func (r HTMLDebug) loadTemplate() *template.Template {
} }
func (r HTML) Render(w http.ResponseWriter) error { func (r HTML) Render(w http.ResponseWriter) error {
writeContentType(w, htmlContentType) r.WriteContentType(w)
if len(r.Name) == 0 { if len(r.Name) == 0 {
return r.Template.Execute(w, r.Data) return r.Template.Execute(w, r.Data)
} }
return r.Template.ExecuteTemplate(w, r.Name, r.Data) return r.Template.ExecuteTemplate(w, r.Name, r.Data)
} }
func (r HTML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, htmlContentType)
}

View File

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

31
render/msgpack.go Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"net/http"
"github.com/ugorji/go/codec"
)
type MsgPack struct {
Data interface{}
}
var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
func (r MsgPack) WriteContentType(w http.ResponseWriter) {
writeContentType(w, msgpackContentType)
}
func (r MsgPack) Render(w http.ResponseWriter) error {
return WriteMsgPack(w, r.Data)
}
func WriteMsgPack(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, msgpackContentType)
var h codec.Handle = new(codec.MsgpackHandle)
return codec.NewEncoder(w, h).Encode(obj)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

13
test_helpers.go Normal file
View File

@ -0,0 +1,13 @@
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
}

91
vendor/vendor.json vendored Normal file
View File

@ -0,0 +1,91 @@
{
"comment": "v1.1.4",
"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": "kBeNcaKk56FguvPSUCEaH6AxpRc=",
"path": "github.com/golang/protobuf/proto",
"revision": "8ee79997227bf9b34611aee7946ae64735e6fd93",
"revisionTime": "2016-11-17T03:31:26Z"
},
{
"checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=",
"path": "github.com/manucorporat/stats",
"revision": "8f2d6ace262eba462e9beb552382c98be51d807b",
"revisionTime": "2015-05-31T20:46:25Z"
},
{
"checksumSHA1": "xZuhljnmBysJPta/lMyYmJdujCg=",
"path": "github.com/mattn/go-isatty",
"revision": "30a891c33c7cde7b02a981314b4228ec99380cca",
"revisionTime": "2016-11-23T14:36:37Z"
},
{
"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": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
"comment": "release-branch.go1.7",
"path": "golang.org/x/net/context",
"revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a",
"revisionTime": "2016-10-18T08:54:36Z"
},
{
"checksumSHA1": "8SH0adTcQlA+W5dzqiQ3Hft2VXg=",
"path": "golang.org/x/sys/unix",
"revision": "478fcf54317e52ab69f40bb4c7a1520288d7f7ea",
"revisionTime": "2016-12-05T15:46:50Z"
},
{
"checksumSHA1": "pyAPYrymvmZl0M/Mr4yfjOQjA8I=",
"path": "gopkg.in/gin-contrib/sse.v0",
"revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae",
"revisionTime": "2017-01-09T09:34:21Z"
},
{
"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"
}