Merge branch 'master' into head

This commit is contained in:
Bo-Yi Wu 2018-08-07 12:41:54 +08:00 committed by GitHub
commit 65858ce114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 332 additions and 18 deletions

View File

@ -28,6 +28,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Querystring parameters](#querystring-parameters) - [Querystring parameters](#querystring-parameters)
- [Multipart/Urlencoded Form](#multiparturlencoded-form) - [Multipart/Urlencoded Form](#multiparturlencoded-form)
- [Another example: query + post form](#another-example-query--post-form) - [Another example: query + post form](#another-example-query--post-form)
- [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
- [Upload files](#upload-files) - [Upload files](#upload-files)
- [Grouping routes](#grouping-routes) - [Grouping routes](#grouping-routes)
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default) - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
@ -324,6 +325,34 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great id: 1234; page: 1; name: manu; message: this_is_great
``` ```
### Map as querystring or postform parameters
```
POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded
names[first]=thinkerou&names[second]=tianou
```
```go
func main() {
router := gin.Default()
router.POST("/post", func(c *gin.Context) {
ids := c.QueryMap("ids")
names := c.PostFormMap("names")
fmt.Printf("ids: %v; names: %v", ids, names)
})
router.Run(":8080")
}
```
```
ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
```
### Upload files ### Upload files
#### Single file #### Single file
@ -901,6 +930,29 @@ func main() {
} }
``` ```
#### AsciiJSON
Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters.
```go
func main() {
r := gin.Default()
r.GET("/someJSON", func(c *gin.Context) {
data := map[string]interface{}{
"lang": "GO语言",
"tag": "<br>",
}
// will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
c.AsciiJSON(http.StatusOK, data)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
```
### Serving static files ### Serving static files
```go ```go

View File

@ -159,16 +159,15 @@ func (c *Context) Error(err error) *Error {
if err == nil { if err == nil {
panic("err is nil") panic("err is nil")
} }
var parsedError *Error
switch err.(type) { parsedError, ok := err.(*Error)
case *Error: if !ok {
parsedError = err.(*Error)
default:
parsedError = &Error{ parsedError = &Error{
Err: err, Err: err,
Type: ErrorTypePrivate, Type: ErrorTypePrivate,
} }
} }
c.Errors = append(c.Errors, parsedError) c.Errors = append(c.Errors, parsedError)
return parsedError return parsedError
} }
@ -361,6 +360,18 @@ func (c *Context) GetQueryArray(key string) ([]string, bool) {
return []string{}, false return []string{}, false
} }
// QueryMap returns a map for a given query key.
func (c *Context) QueryMap(key string) map[string]string {
dicts, _ := c.GetQueryMap(key)
return dicts
}
// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
return c.get(c.Request.URL.Query(), key)
}
// PostForm returns the specified key from a POST urlencoded form or multipart form // PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`. // when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) string { func (c *Context) PostForm(key string) string {
@ -416,6 +427,42 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false return []string{}, false
} }
// PostFormMap returns a map for a given form key.
func (c *Context) PostFormMap(key string) map[string]string {
dicts, _ := c.GetPostFormMap(key)
return dicts
}
// GetPostFormMap returns a map for a given form key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
req := c.Request
req.ParseForm()
req.ParseMultipartForm(c.engine.MaxMultipartMemory)
dicts, exist := c.get(req.PostForm, key)
if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
dicts, exist = c.get(req.MultipartForm.Value, key)
}
return dicts, exist
}
// get is an internal method and returns a map which satisfy conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
dicts := make(map[string]string)
exist := false
for k, v := range m {
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
exist = true
dicts[k[i+1:][:j]] = v[0]
}
}
}
return dicts, exist
}
// FormFile returns the first file for the provided form key. // FormFile returns the first file for the provided form key.
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) { func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
_, fh, err := c.Request.FormFile(name) _, fh, err := c.Request.FormFile(name)
@ -695,7 +742,12 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
// It add padding to response body to request data from a server residing in a different domain than the client. // It add padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript". // It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj interface{}) { func (c *Context) JSONP(code int, obj interface{}) {
c.Render(code, render.JsonpJSON{Callback: c.DefaultQuery("callback", ""), Data: obj}) callback := c.DefaultQuery("callback", "")
if callback == "" {
c.Render(code, render.JSON{Data: obj})
} else {
c.Render(code, render.JsonpJSON{Callback: callback, Data: obj})
}
} }
// JSON serializes the given struct as JSON into the response body. // JSON serializes the given struct as JSON into the response body.
@ -704,6 +756,12 @@ func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj}) c.Render(code, render.JSON{Data: obj})
} }
// AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string.
// It also sets the Content-Type as "application/json".
func (c *Context) AsciiJSON(code int, obj interface{}) {
c.Render(code, render.AsciiJSON{Data: obj})
}
// XML serializes the given struct as XML into the response body. // XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml". // It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) { func (c *Context) XML(code int, obj interface{}) {

View File

@ -21,6 +21,7 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/context" "golang.org/x/net/context"
"io"
) )
var _ context.Context = &Context{} var _ context.Context = &Context{}
@ -47,6 +48,8 @@ func createMultipartRequest() *http.Request {
must(mw.WriteField("time_local", "31/12/2016 14:55")) must(mw.WriteField("time_local", "31/12/2016 14:55"))
must(mw.WriteField("time_utc", "31/12/2016 14:55")) must(mw.WriteField("time_utc", "31/12/2016 14:55"))
must(mw.WriteField("time_location", "31/12/2016 14:55")) must(mw.WriteField("time_location", "31/12/2016 14:55"))
must(mw.WriteField("names[a]", "thinkerou"))
must(mw.WriteField("names[b]", "tianou"))
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)
@ -371,7 +374,8 @@ func TestContextQuery(t *testing.T) {
func TestContextQueryAndPostForm(t *testing.T) { func TestContextQueryAndPostForm(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) 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&ids[a]=hi&ids[b]=3.14", body)
c.Request.Header.Add("Content-Type", MIMEPOSTForm) c.Request.Header.Add("Content-Type", MIMEPOSTForm)
assert.Equal(t, "bar", c.DefaultPostForm("foo", "none")) assert.Equal(t, "bar", c.DefaultPostForm("foo", "none"))
@ -439,6 +443,30 @@ func TestContextQueryAndPostForm(t *testing.T) {
values = c.QueryArray("both") values = c.QueryArray("both")
assert.Equal(t, 1, len(values)) assert.Equal(t, 1, len(values))
assert.Equal(t, "GET", values[0]) assert.Equal(t, "GET", values[0])
dicts, ok := c.GetQueryMap("ids")
assert.True(t, ok)
assert.Equal(t, "hi", dicts["a"])
assert.Equal(t, "3.14", dicts["b"])
dicts, ok = c.GetQueryMap("nokey")
assert.False(t, ok)
assert.Equal(t, 0, len(dicts))
dicts, ok = c.GetQueryMap("both")
assert.False(t, ok)
assert.Equal(t, 0, len(dicts))
dicts, ok = c.GetQueryMap("array")
assert.False(t, ok)
assert.Equal(t, 0, len(dicts))
dicts = c.QueryMap("ids")
assert.Equal(t, "hi", dicts["a"])
assert.Equal(t, "3.14", dicts["b"])
dicts = c.QueryMap("nokey")
assert.Equal(t, 0, len(dicts))
} }
func TestContextPostFormMultipart(t *testing.T) { func TestContextPostFormMultipart(t *testing.T) {
@ -515,6 +543,22 @@ func TestContextPostFormMultipart(t *testing.T) {
values = c.PostFormArray("foo") values = c.PostFormArray("foo")
assert.Equal(t, 1, len(values)) assert.Equal(t, 1, len(values))
assert.Equal(t, "bar", values[0]) assert.Equal(t, "bar", values[0])
dicts, ok := c.GetPostFormMap("names")
assert.True(t, ok)
assert.Equal(t, "thinkerou", dicts["a"])
assert.Equal(t, "tianou", dicts["b"])
dicts, ok = c.GetPostFormMap("nokey")
assert.False(t, ok)
assert.Equal(t, 0, len(dicts))
dicts = c.PostFormMap("names")
assert.Equal(t, "thinkerou", dicts["a"])
assert.Equal(t, "tianou", dicts["b"])
dicts = c.PostFormMap("nokey")
assert.Equal(t, 0, len(dicts))
} }
func TestContextSetCookie(t *testing.T) { func TestContextSetCookie(t *testing.T) {
@ -596,6 +640,20 @@ func TestContextRenderJSONP(t *testing.T) {
assert.Equal(t, "application/javascript; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/javascript; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// Tests that the response is serialized as JSONP
// and Content-Type is set to application/json
func TestContextRenderJSONPWithoutCallback(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://example.com", nil)
c.JSONP(201, H{"foo": "bar"})
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 // Tests that no JSON is rendered if code is 204
func TestContextRenderNoContentJSON(t *testing.T) { func TestContextRenderNoContentJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -686,6 +744,17 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
func TestContextRenderNoContentAsciiJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.AsciiJSON(http.StatusNoContent, []string{"lang", "Go语言"})
assert.Equal(t, http.StatusNoContent, w.Code)
assert.Empty(t, w.Body.String())
assert.Equal(t, "application/json", w.HeaderMap.Get("Content-Type"))
}
// 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) {
@ -1490,3 +1559,38 @@ func TestContextRenderDataFromReader(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%d", contentLength), w.HeaderMap.Get("Content-Length")) assert.Equal(t, fmt.Sprintf("%d", contentLength), w.HeaderMap.Get("Content-Length"))
assert.Equal(t, extraHeaders["Content-Disposition"], w.HeaderMap.Get("Content-Disposition")) assert.Equal(t, extraHeaders["Content-Disposition"], w.HeaderMap.Get("Content-Disposition"))
} }
func TestContextStream(t *testing.T) {
w := CreateTestResponseRecorder()
c, _ := CreateTestContext(w)
stopStream := true
c.Stream(func(w io.Writer) bool {
defer func() {
stopStream = false
}()
w.Write([]byte("test"))
return stopStream
})
assert.Equal(t, "testtest", w.Body.String())
}
func TestContextStreamWithClientGone(t *testing.T) {
w := CreateTestResponseRecorder()
c, _ := CreateTestContext(w)
c.Stream(func(writer io.Writer) bool {
defer func() {
w.closeClient()
}()
writer.Write([]byte("test"))
return true
})
assert.Equal(t, "test", w.Body.String())
}

View File

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

View File

@ -128,7 +128,7 @@ func Run(addr ...string) (err error) {
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests. // RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens. // Note: this method will block the calling goroutine undefinitelly unless an error happens.
func RunTLS(addr string, certFile string, keyFile string) (err error) { func RunTLS(addr, certFile, keyFile string) (err error) {
return engine().RunTLS(addr, certFile, keyFile) return engine().RunTLS(addr, certFile, keyFile)
} }

View File

@ -6,6 +6,7 @@ package render
import ( import (
"bytes" "bytes"
"fmt"
"html/template" "html/template"
"net/http" "net/http"
@ -30,10 +31,15 @@ type JsonpJSON struct {
Data interface{} Data interface{}
} }
type AsciiJSON struct {
Data interface{}
}
type SecureJSONPrefix string type SecureJSONPrefix string
var jsonContentType = []string{"application/json; charset=utf-8"} var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"} var jsonpContentType = []string{"application/javascript; charset=utf-8"}
var jsonAsciiContentType = []string{"application/json"}
func (r JSON) Render(w http.ResponseWriter) (err error) { func (r JSON) Render(w http.ResponseWriter) (err error) {
if err = WriteJSON(w, r.Data); err != nil { if err = WriteJSON(w, r.Data); err != nil {
@ -112,3 +118,29 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
func (r JsonpJSON) WriteContentType(w http.ResponseWriter) { func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonpContentType) writeContentType(w, jsonpContentType)
} }
func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w)
ret, err := json.Marshal(r.Data)
if err != nil {
return err
}
var buffer bytes.Buffer
for _, r := range string(ret) {
cvt := ""
if r < 128 {
cvt = string(r)
} else {
cvt = fmt.Sprintf("\\u%04x", int64(r))
}
buffer.WriteString(cvt)
}
w.Write(buffer.Bytes())
return nil
}
func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonAsciiContentType)
}

View File

@ -26,6 +26,7 @@ var (
_ Render = YAML{} _ Render = YAML{}
_ Render = MsgPack{} _ Render = MsgPack{}
_ Render = Reader{} _ Render = Reader{}
_ Render = AsciiJSON{}
) )
func writeContentType(w http.ResponseWriter, value []string) { func writeContentType(w http.ResponseWriter, value []string) {

View File

@ -167,6 +167,35 @@ func TestRenderJsonpJSONFail(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestRenderAsciiJSON(t *testing.T) {
w1 := httptest.NewRecorder()
data1 := map[string]interface{}{
"lang": "GO语言",
"tag": "<br>",
}
err := (AsciiJSON{data1}).Render(w1)
assert.NoError(t, err)
assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
w2 := httptest.NewRecorder()
data2 := float64(3.1415926)
err = (AsciiJSON{data2}).Render(w2)
assert.NoError(t, err)
assert.Equal(t, "3.1415926", w2.Body.String())
}
func TestRenderAsciiJSONFail(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
// json: unsupported type: chan int
assert.Error(t, (AsciiJSON{data}).Render(w))
}
type xmlmap map[string]interface{} type xmlmap map[string]interface{}
// Allows type H to be used with xml.Marshal // Allows type H to be used with xml.Marshal

View File

@ -110,5 +110,6 @@ func (w *responseWriter) CloseNotify() <-chan bool {
// Flush implements the http.Flush interface. // Flush implements the http.Flush interface.
func (w *responseWriter) Flush() { func (w *responseWriter) Flush() {
w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush() w.ResponseWriter.(http.Flusher).Flush()
} }

View File

@ -51,7 +51,7 @@ func TestResponseWriterWriteHeader(t *testing.T) {
w.WriteHeader(300) w.WriteHeader(300)
assert.False(t, w.Written()) assert.False(t, w.Written())
assert.Equal(t, 300, w.Status()) assert.Equal(t, 300, w.Status())
assert.NotEqual(t, testWritter.Code, 300) assert.NotEqual(t, 300, testWritter.Code)
w.WriteHeader(-1) w.WriteHeader(-1)
assert.Equal(t, 300, w.Status()) assert.Equal(t, 300, w.Status())
@ -113,3 +113,19 @@ func TestResponseWriterHijack(t *testing.T) {
w.Flush() w.Flush()
} }
func TestResponseWriterFlush(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writer := &responseWriter{}
writer.reset(w)
writer.WriteHeader(http.StatusInternalServerError)
writer.Flush()
}))
defer testServer.Close()
// should return 500
resp, err := http.Get(testServer.URL)
assert.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
}

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"net/http" "net/http"
"net/http/httptest"
) )
// CreateTestContext returns a fresh engine and context for testing purposes // CreateTestContext returns a fresh engine and context for testing purposes
@ -16,3 +17,23 @@ func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
c.writermem.reset(w) c.writermem.reset(w)
return return
} }
type TestResponseRecorder struct {
*httptest.ResponseRecorder
closeChannel chan bool
}
func (r *TestResponseRecorder) CloseNotify() <-chan bool {
return r.closeChannel
}
func (r *TestResponseRecorder) closeClient() {
r.closeChannel <- true
}
func CreateTestResponseRecorder() *TestResponseRecorder {
return &TestResponseRecorder{
httptest.NewRecorder(),
make(chan bool, 1),
}
}