From 30f014c7540d42fdc4035587621dbc2b3084bd19 Mon Sep 17 00:00:00 2001 From: Danieliu Date: Thu, 26 May 2016 16:21:50 +0800 Subject: [PATCH 01/36] fix default log format `reset` field should be after `method` in LoggerWithWriter function. --- logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.go b/logger.go index d56bc628..ff246a44 100644 --- a/logger.go +++ b/logger.go @@ -80,7 +80,7 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { statusColor, statusCode, reset, latency, clientIP, - methodColor, reset, method, + methodColor, method, reset, path, comment, ) From 8464b14974fe89a347febccfa37ef013f2c3c75e Mon Sep 17 00:00:00 2001 From: Tatsuya Hoshino Date: Wed, 8 Jun 2016 07:37:09 +0900 Subject: [PATCH 02/36] Remove an obsolete func in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8023dc5a..d6fddf41 100644 --- a/README.md +++ b/README.md @@ -468,7 +468,7 @@ func main() { ####HTML rendering -Using LoadHTMLTemplates() +Using LoadHTMLGlob() or LoadHTMLFiles() ```go func main() { From bf8da4a08a99059f2e06da811be9cb08f0cf0a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Soul=C3=A9?= Date: Fri, 9 Sep 2016 11:37:22 +0200 Subject: [PATCH 03/36] Context.Get() does not need to test whether Keys is nil or not --- context.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/context.go b/context.go index 5d3b6a4e..50161750 100644 --- a/context.go +++ b/context.go @@ -166,9 +166,7 @@ func (c *Context) Set(key string, value interface{}) { // Get returns the value for the given key, ie: (value, true). // If the value does not exists it returns (nil, false) func (c *Context) Get(key string) (value interface{}, exists bool) { - if c.Keys != nil { - value, exists = c.Keys[key] - } + value, exists = c.Keys[key] return } From 61ba9db5af23da0f11d5f68bced9ccc5a417859b Mon Sep 17 00:00:00 2001 From: Yehezkiel Syamsuhadi Date: Wed, 21 Sep 2016 12:16:51 +1000 Subject: [PATCH 04/36] Make CreateTestContext public without importing net/http/httptest --- context_test.go | 115 +++++++++++++++++++++++++++++++----------------- helpers_test.go | 14 ------ test_helpers.go | 13 ++++++ 3 files changed, 88 insertions(+), 54 deletions(-) delete mode 100644 helpers_test.go create mode 100644 test_helpers.go diff --git a/context_test.go b/context_test.go index 97d4957c..c7a1fc89 100644 --- a/context_test.go +++ b/context_test.go @@ -74,7 +74,7 @@ func TestContextReset(t *testing.T) { } func TestContextHandlers(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) assert.Nil(t, c.handlers) assert.Nil(t, c.handlers.Last()) @@ -95,7 +95,7 @@ func TestContextHandlers(t *testing.T) { // TestContextSetGet tests that a parameter is set correctly on the // current context and can be retrieved using Get. func TestContextSetGet(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Set("foo", "bar") value, err := c.Get("foo") @@ -111,7 +111,7 @@ func TestContextSetGet(t *testing.T) { } func TestContextSetGetValues(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Set("string", "this is a string") c.Set("int32", int32(-42)) c.Set("int64", int64(42424242424242)) @@ -132,7 +132,7 @@ func TestContextSetGetValues(t *testing.T) { } func TestContextCopy(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.index = 2 c.Request, _ = http.NewRequest("POST", "/hola", nil) c.handlers = HandlersChain{func(c *Context) {}} @@ -151,7 +151,7 @@ func TestContextCopy(t *testing.T) { } func TestContextHandlerName(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest} assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName()) @@ -162,7 +162,7 @@ func handlerNameTest(c *Context) { } func TestContextQuery(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil) value, ok := c.GetQuery("foo") @@ -197,7 +197,7 @@ func TestContextQuery(t *testing.T) { } func TestContextQueryAndPostForm(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second") c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body) c.Request.Header.Add("Content-Type", MIMEPOSTForm) @@ -254,7 +254,7 @@ func TestContextQueryAndPostForm(t *testing.T) { } func TestContextPostFormMultipart(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request = createMultipartRequest() var obj struct { @@ -302,13 +302,13 @@ func TestContextPostFormMultipart(t *testing.T) { } func TestContextSetCookie(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.SetCookie("user", "gin", 1, "/", "localhost", true, true) assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") } func TestContextGetCookie(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("GET", "/get", nil) c.Request.Header.Set("Cookie", "user=gin") cookie, _ := c.Cookie("user") @@ -318,7 +318,9 @@ func TestContextGetCookie(t *testing.T) { // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderJSON(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.JSON(201, H{"foo": "bar"}) assert.Equal(t, w.Code, 201) @@ -329,7 +331,9 @@ func TestContextRenderJSON(t *testing.T) { // Tests that the response is serialized as JSON // we change the content-type before func TestContextRenderAPIJSON(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Header("Content-Type", "application/vnd.api+json") c.JSON(201, H{"foo": "bar"}) @@ -341,7 +345,9 @@ func TestContextRenderAPIJSON(t *testing.T) { // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderIndentedJSON(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) assert.Equal(t, w.Code, 201) @@ -352,7 +358,8 @@ func TestContextRenderIndentedJSON(t *testing.T) { // Tests that the response executes the templates // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { - c, w, router := CreateTestContext() + w := httptest.NewRecorder() + c, router := CreateTestContext(w) templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) router.SetHTMLTemplate(templ) @@ -366,7 +373,9 @@ func TestContextRenderHTML(t *testing.T) { // TestContextXML tests that the response is serialized as XML // and Content-Type is set to application/xml func TestContextRenderXML(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.XML(201, H{"foo": "bar"}) assert.Equal(t, w.Code, 201) @@ -377,7 +386,9 @@ func TestContextRenderXML(t *testing.T) { // TestContextString tests that the response is returned // with Content-Type set to text/plain func TestContextRenderString(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.String(201, "test %s %d", "string", 2) assert.Equal(t, w.Code, 201) @@ -388,7 +399,9 @@ func TestContextRenderString(t *testing.T) { // TestContextString tests that the response is returned // with Content-Type set to text/html func TestContextRenderHTMLString(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Header("Content-Type", "text/html; charset=utf-8") c.String(201, "%s %d", "string", 3) @@ -400,7 +413,9 @@ func TestContextRenderHTMLString(t *testing.T) { // TestContextData tests that the response can be written from `bytesting` // with specified MIME type func TestContextRenderData(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Data(201, "text/csv", []byte(`foo,bar`)) assert.Equal(t, w.Code, 201) @@ -409,7 +424,9 @@ func TestContextRenderData(t *testing.T) { } func TestContextRenderSSE(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.SSEvent("float", 1.5) c.Render(-1, sse.Event{ Id: "123", @@ -424,7 +441,9 @@ func TestContextRenderSSE(t *testing.T) { } func TestContextRenderFile(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("GET", "/", nil) c.File("./gin.go") @@ -436,7 +455,9 @@ func TestContextRenderFile(t *testing.T) { // TestContextRenderYAML tests that the response is serialized as YAML // and Content-Type is set to application/x-yaml func TestContextRenderYAML(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.YAML(201, H{"foo": "bar"}) assert.Equal(t, w.Code, 201) @@ -445,7 +466,7 @@ func TestContextRenderYAML(t *testing.T) { } func TestContextHeaders(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Header("Content-Type", "text/plain") c.Header("X-Custom", "value") @@ -462,7 +483,9 @@ func TestContextHeaders(t *testing.T) { // TODO func TestContextRenderRedirectWithRelativePath(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "http://example.com", nil) assert.Panics(t, func() { c.Redirect(299, "/new_path") }) assert.Panics(t, func() { c.Redirect(309, "/new_path") }) @@ -474,7 +497,9 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) { } func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Redirect(302, "http://google.com") c.Writer.WriteHeaderNow() @@ -484,7 +509,9 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { } func TestContextRenderRedirectWith201(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "http://example.com", nil) c.Redirect(201, "/resource") c.Writer.WriteHeaderNow() @@ -494,7 +521,7 @@ func TestContextRenderRedirectWith201(t *testing.T) { } func TestContextRenderRedirectAll(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "http://example.com", nil) assert.Panics(t, func() { c.Redirect(200, "/resource") }) assert.Panics(t, func() { c.Redirect(202, "/resource") }) @@ -505,7 +532,7 @@ func TestContextRenderRedirectAll(t *testing.T) { } func TestContextNegotiationFormat(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "", nil) assert.Panics(t, func() { c.NegotiateFormat() }) @@ -514,7 +541,7 @@ func TestContextNegotiationFormat(t *testing.T) { } func TestContextNegotiationFormatWithAccept(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") @@ -524,7 +551,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) { } func TestContextNegotiationFormatCustum(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") @@ -537,7 +564,7 @@ func TestContextNegotiationFormatCustum(t *testing.T) { } func TestContextIsAborted(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) assert.False(t, c.IsAborted()) c.Abort() @@ -553,7 +580,9 @@ func TestContextIsAborted(t *testing.T) { // TestContextData tests that the response can be written from `bytesting` // with specified MIME type func TestContextAbortWithStatus(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.index = 4 c.AbortWithStatus(401) @@ -564,7 +593,7 @@ func TestContextAbortWithStatus(t *testing.T) { } func TestContextError(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) assert.Empty(t, c.Errors) c.Error(errors.New("first error")) @@ -590,7 +619,7 @@ func TestContextError(t *testing.T) { } func TestContextTypedError(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) @@ -604,7 +633,9 @@ func TestContextTypedError(t *testing.T) { } func TestContextAbortWithError(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.AbortWithError(401, errors.New("bad input")).SetMeta("some input") assert.Equal(t, w.Code, 401) @@ -613,7 +644,7 @@ func TestContextAbortWithError(t *testing.T) { } func TestContextClientIP(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") @@ -633,7 +664,7 @@ func TestContextClientIP(t *testing.T) { } func TestContextContentType(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", nil) c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") @@ -641,7 +672,7 @@ func TestContextContentType(t *testing.T) { } func TestContextAutoBindJSON(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request.Header.Add("Content-Type", MIMEJSON) @@ -656,7 +687,9 @@ func TestContextAutoBindJSON(t *testing.T) { } func TestContextBindWithJSON(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type @@ -671,7 +704,9 @@ func TestContextBindWithJSON(t *testing.T) { } func TestContextBadAutoBind(t *testing.T) { - c, w, _ := CreateTestContext() + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request.Header.Add("Content-Type", MIMEJSON) var obj struct { @@ -690,7 +725,7 @@ func TestContextBadAutoBind(t *testing.T) { } func TestContextGolangContext(t *testing.T) { - c, _, _ := CreateTestContext() + c, _ := CreateTestContext(httptest.NewRecorder()) c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) assert.NoError(t, c.Err()) assert.Nil(t, c.Done()) diff --git a/helpers_test.go b/helpers_test.go deleted file mode 100644 index 7d8020c3..00000000 --- a/helpers_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package gin - -import ( - "net/http/httptest" -) - -func CreateTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { - w = httptest.NewRecorder() - r = New() - c = r.allocateContext() - c.reset() - c.writermem.reset(w) - return -} diff --git a/test_helpers.go b/test_helpers.go new file mode 100644 index 00000000..5bb3fa71 --- /dev/null +++ b/test_helpers.go @@ -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 +} From ceb250ba20bea1721fa087b3db800926355287c8 Mon Sep 17 00:00:00 2001 From: Vyacheslav Dubinin Date: Wed, 19 Oct 2016 17:13:38 +0300 Subject: [PATCH 05/36] Move golang.org/x/net/context.Context interface implementation check to tests --- context.go | 3 --- context_test.go | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 5d3b6a4e..d0fa7cb3 100644 --- a/context.go +++ b/context.go @@ -17,7 +17,6 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" "github.com/manucorporat/sse" - "golang.org/x/net/context" ) // Content-Type MIME of the most common data formats @@ -50,8 +49,6 @@ type Context struct { Accepted []string } -var _ context.Context = &Context{} - /************************************/ /********** CONTEXT CREATION ********/ /************************************/ diff --git a/context_test.go b/context_test.go index 97d4957c..a1f9fa3d 100644 --- a/context_test.go +++ b/context_test.go @@ -17,8 +17,11 @@ import ( "github.com/manucorporat/sse" "github.com/stretchr/testify/assert" + "golang.org/x/net/context" ) +var _ context.Context = &Context{} + // Unit tests TODO // func (c *Context) File(filepath string) { // func (c *Context) Negotiate(code int, config Negotiate) { From c8b35d34452e9c1fc34d0aec480d6fa7e3092584 Mon Sep 17 00:00:00 2001 From: Tevin Jeffrey Date: Sun, 29 May 2016 16:08:24 -0400 Subject: [PATCH 06/36] Fix for #630 Details can be found in issue #630 --- errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors.go b/errors.go index 7716bfae..694b428a 100644 --- a/errors.go +++ b/errors.go @@ -72,7 +72,7 @@ func (msg *Error) MarshalJSON() ([]byte, error) { } // Implements the error interface -func (msg *Error) Error() string { +func (msg Error) Error() string { return msg.Err.Error() } From 7e58c80a7c8e299183d947cd78affb8f60fbb5de Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Mon, 5 Dec 2016 11:21:59 +0100 Subject: [PATCH 07/36] Fix #723 --- context.go | 8 ++++++++ context_appengine.go | 7 +++++++ context_test.go | 9 +++++++++ gin.go | 6 ++++++ 4 files changed, 30 insertions(+) create mode 100644 context_appengine.go diff --git a/context.go b/context.go index df001a40..ffda5ab1 100644 --- a/context.go +++ b/context.go @@ -353,9 +353,17 @@ func (c *Context) ClientIP() string { return clientIP } } + + if c.engine.AppEngine { + if addr := c.Request.Header.Get("X-Appengine-Remote-Addr"); addr != "" { + return addr + } + } + if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil { return ip } + return "" } diff --git a/context_appengine.go b/context_appengine.go new file mode 100644 index 00000000..d9cb22f3 --- /dev/null +++ b/context_appengine.go @@ -0,0 +1,7 @@ +// +build appengine + +package gin + +func init() { + defaultAppEngine = true +} diff --git a/context_test.go b/context_test.go index 01ee6b83..d46be714 100644 --- a/context_test.go +++ b/context_test.go @@ -650,6 +650,7 @@ func TestContextClientIP(t *testing.T) { c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") + c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50") c.Request.RemoteAddr = " 40.40.40.40:42123 " assert.Equal(t, c.ClientIP(), "10.10.10.10") @@ -661,7 +662,15 @@ func TestContextClientIP(t *testing.T) { assert.Equal(t, c.ClientIP(), "30.30.30.30") c.Request.Header.Del("X-Forwarded-For") + c.engine.AppEngine = true + assert.Equal(t, c.ClientIP(), "50.50.50.50") + + c.Request.Header.Del("X-Appengine-Remote-Addr") assert.Equal(t, c.ClientIP(), "40.40.40.40") + + // no port + c.Request.RemoteAddr = "50.50.50.50" + assert.Equal(t, c.ClientIP(), "") } func TestContextContentType(t *testing.T) { diff --git a/gin.go b/gin.go index 60f4ab29..70cc11b6 100644 --- a/gin.go +++ b/gin.go @@ -19,6 +19,7 @@ const Version = "v1.0rc2" var default404Body = []byte("404 page not found") var default405Body = []byte("405 method not allowed") +var defaultAppEngine bool type HandlerFunc func(*Context) type HandlersChain []HandlerFunc @@ -78,6 +79,10 @@ type ( // handler. HandleMethodNotAllowed bool ForwardedByClientIP bool + + // #726 #755 If enabled, it will thrust some headers starting with + // 'X-AppEngine...' for better integration with that PaaS. + AppEngine bool } ) @@ -101,6 +106,7 @@ func New() *Engine { RedirectFixedPath: false, HandleMethodNotAllowed: false, ForwardedByClientIP: true, + AppEngine: defaultAppEngine, trees: make(methodTrees, 0, 9), } engine.RouterGroup.engine = engine From 98af44604c755936c31d990f7135b34e1433095f Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Tue, 6 Dec 2016 14:38:14 +0100 Subject: [PATCH 08/36] Update const framework version to 1.1.4 --- gin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gin.go b/gin.go index 60f4ab29..75adc473 100644 --- a/gin.go +++ b/gin.go @@ -15,7 +15,7 @@ import ( ) // Version is Framework's version -const Version = "v1.0rc2" +const Version = "v1.1.4" var default404Body = []byte("404 page not found") var default405Body = []byte("405 method not allowed") From 2033b73551ffcba0002bfec84cfefc4b53b144fe Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Tue, 6 Dec 2016 19:53:58 +0100 Subject: [PATCH 09/36] Migrate from godeps to govendor --- Godeps/Godeps.json | 36 ---------------------------------- vendor/vendor.json | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 36 deletions(-) delete mode 100644 Godeps/Godeps.json create mode 100644 vendor/vendor.json diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json deleted file mode 100644 index a9c828a2..00000000 --- a/Godeps/Godeps.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "ImportPath": "github.com/gin-gonic/gin", - "GoVersion": "go1.5.1", - "Deps": [ - { - "ImportPath": "github.com/davecgh/go-spew/spew", - "Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d" - }, - { - "ImportPath": "github.com/golang/protobuf/proto", - "Rev": "2402d76f3d41f928c7902a765dfc872356dd3aad" - }, - { - "ImportPath": "github.com/manucorporat/sse", - "Rev": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d" - }, - { - "ImportPath": "github.com/pmezard/go-difflib/difflib", - "Rev": "792786c7400a136282c1664665ae0a8db921c6c2" - }, - { - "ImportPath": "github.com/stretchr/testify/assert", - "Comment": "v1.1.3", - "Rev": "f390dcf405f7b83c997eac1b06768bb9f44dec18" - }, - { - "ImportPath": "golang.org/x/net/context", - "Rev": "f315505cf3349909cdf013ea56690da34e96a451" - }, - { - "ImportPath": "gopkg.in/go-playground/validator.v8", - "Comment": "v8.15.1", - "Rev": "c193cecd124b5cc722d7ee5538e945bdb3348435" - } - ] -} diff --git a/vendor/vendor.json b/vendor/vendor.json new file mode 100644 index 00000000..3f60f407 --- /dev/null +++ b/vendor/vendor.json @@ -0,0 +1,49 @@ +{ + "comment": "", + "ignore": "test", + "package": [ + { + "path": "github.com/davecgh/go-spew/spew", + "revision": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d" + }, + { + "path": "github.com/golang/protobuf/proto", + "revision": "2402d76f3d41f928c7902a765dfc872356dd3aad" + }, + { + "path": "github.com/golang/protobuf/proto/proto3_proto", + "revision": "2402d76f3d41f928c7902a765dfc872356dd3aad" + }, + { + "path": "github.com/golang/protobuf/proto/testdata", + "revision": "2402d76f3d41f928c7902a765dfc872356dd3aad" + }, + { + "path": "github.com/golang/protobuf/protoc-gen-go/descriptor", + "revision": "2402d76f3d41f928c7902a765dfc872356dd3aad" + }, + { + "path": "github.com/manucorporat/sse", + "revision": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d" + }, + { + "path": "github.com/pmezard/go-difflib/difflib", + "revision": "792786c7400a136282c1664665ae0a8db921c6c2" + }, + { + "comment": "v1.1.3", + "path": "github.com/stretchr/testify/assert", + "revision": "f390dcf405f7b83c997eac1b06768bb9f44dec18" + }, + { + "path": "golang.org/x/net/context", + "revision": "f315505cf3349909cdf013ea56690da34e96a451" + }, + { + "comment": "v8.15.1", + "path": "gopkg.in/go-playground/validator.v8", + "revision": "c193cecd124b5cc722d7ee5538e945bdb3348435" + } + ], + "rootPath": "github.com/gin-gonic/gin" +} From 44529e4a348c1d76594237677cec72eaa2753beb Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Tue, 6 Dec 2016 23:53:49 +0100 Subject: [PATCH 10/36] Update vendor.json --- vendor/vendor.json | 80 +++++++++++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/vendor/vendor.json b/vendor/vendor.json index 3f60f407..3c6b0edb 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1,48 +1,84 @@ { - "comment": "", + "comment": "v1.1.4", "ignore": "test", "package": [ { + "checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=", + "comment": "v1.1.0", "path": "github.com/davecgh/go-spew/spew", - "revision": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d" + "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": "2402d76f3d41f928c7902a765dfc872356dd3aad" - }, - { - "path": "github.com/golang/protobuf/proto/proto3_proto", - "revision": "2402d76f3d41f928c7902a765dfc872356dd3aad" - }, - { - "path": "github.com/golang/protobuf/proto/testdata", - "revision": "2402d76f3d41f928c7902a765dfc872356dd3aad" - }, - { - "path": "github.com/golang/protobuf/protoc-gen-go/descriptor", - "revision": "2402d76f3d41f928c7902a765dfc872356dd3aad" + "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", + "revisionTime": "2016-11-17T03:31:26Z" }, { + "checksumSHA1": "b0T0Hzd+zYk+OCDTFMps+jwa/nY=", "path": "github.com/manucorporat/sse", - "revision": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d" + "revision": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d", + "revisionTime": "2016-01-26T18:01:36Z" }, { + "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" + "revision": "792786c7400a136282c1664665ae0a8db921c6c2", + "revisionTime": "2016-01-10T10:55:54Z" }, { - "comment": "v1.1.3", + "checksumSHA1": "Q2V7Zs3diLmLfmfbiuLpSxETSuY=", + "comment": "v1.1.4", "path": "github.com/stretchr/testify/assert", - "revision": "f390dcf405f7b83c997eac1b06768bb9f44dec18" + "revision": "976c720a22c8eb4eb6a0b4348ad85ad12491a506", + "revisionTime": "2016-09-25T22:06:09Z" }, { + "checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=", + "comment": "release-branch.go1.7", "path": "golang.org/x/net/context", - "revision": "f315505cf3349909cdf013ea56690da34e96a451" + "revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a", + "revisionTime": "2016-10-18T08:54:36Z" }, { - "comment": "v8.15.1", + "checksumSHA1": "8SH0adTcQlA+W5dzqiQ3Hft2VXg=", + "path": "golang.org/x/sys/unix", + "revision": "478fcf54317e52ab69f40bb4c7a1520288d7f7ea", + "revisionTime": "2016-12-05T15:46:50Z" + }, + { + "checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=", + "comment": "v8.18.1", "path": "gopkg.in/go-playground/validator.v8", - "revision": "c193cecd124b5cc722d7ee5538e945bdb3348435" + "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" From abcbfb5d68b49ebf96b24c0eff637806176cb4f1 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Tue, 6 Dec 2016 23:54:11 +0100 Subject: [PATCH 11/36] Update .travis.yml --- .travis.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 53f436f4..e6a05bd0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,17 @@ language: go sudo: false go: - - 1.4 - - 1.5.4 - 1.6.4 - 1.7.4 - tip +git: + depth: 3 + +install: + - go get -v github.com/kardianos/govendor + - govendor sync + script: - go test -v -covermode=count -coverprofile=coverage.out From 97d310b55ca24d9c0829aaff61ff646123f49442 Mon Sep 17 00:00:00 2001 From: chriswhelix Date: Tue, 15 Nov 2016 15:51:05 -0800 Subject: [PATCH 12/36] Honor normal gin write contract for context.JSON() Gin normally silently swallows errors writing to the client; however in WriteJSON (and thus context.JSON), the ResponseWriter was being passed directly into the JSON encoder, which will return an error if there's an error writing to the stream. For instance, context.JSON would panic with errors like "write tcp XXX-> YYY: write: connection reset by peer" if the client disconnected before the response was complete. This change makes JSON.Render() treat write errors the same as IndentedJSON, Data, and other renderers. --- render/json.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/render/json.go b/render/json.go index 32e6058d..b652c4c8 100644 --- a/render/json.go +++ b/render/json.go @@ -37,5 +37,10 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error { func WriteJSON(w http.ResponseWriter, obj interface{}) error { writeContentType(w, jsonContentType) - return json.NewEncoder(w).Encode(obj) + jsonBytes, err := json.Marshal(obj) + if err != nil { + return err + } + w.Write(jsonBytes) + return nil } From 787bff85e58c5361ffe6c5d3b2bd261a65cf52c6 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sun, 11 Dec 2016 10:14:20 +0800 Subject: [PATCH 13/36] fix testing. Signed-off-by: Bo-Yi Wu --- context_test.go | 12 ++++++------ logger_test.go | 12 ++++++------ middleware_test.go | 4 ++-- render/render_test.go | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/context_test.go b/context_test.go index 32e596e0..81e18841 100644 --- a/context_test.go +++ b/context_test.go @@ -358,9 +358,9 @@ func TestContextRenderJSON(t *testing.T) { c.JSON(201, H{"foo": "bar"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } // Tests that the response is serialized as JSON @@ -372,9 +372,9 @@ func TestContextRenderAPIJSON(t *testing.T) { c.Header("Content-Type", "application/vnd.api+json") c.JSON(201, H{"foo": "bar"}) - assert.Equal(t, w.Code, 201) - assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") - assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json") + assert.Equal(t, 201, w.Code) + assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type")) } // Tests that the response is serialized as JSON diff --git a/logger_test.go b/logger_test.go index 2ad1f474..7c064c90 100644 --- a/logger_test.go +++ b/logger_test.go @@ -107,16 +107,16 @@ func TestErrorLogger(t *testing.T) { }) w := performRequest(router, "GET", "/error") - assert.Equal(t, w.Code, 200) - assert.Equal(t, w.Body.String(), "{\"error\":\"this is an error\"}\n") + assert.Equal(t, 200, w.Code) + assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String()) w = performRequest(router, "GET", "/abort") - assert.Equal(t, w.Code, 401) - assert.Equal(t, w.Body.String(), "{\"error\":\"no authorized\"}\n") + assert.Equal(t, 401, w.Code) + assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String()) w = performRequest(router, "GET", "/print") - assert.Equal(t, w.Code, 500) - assert.Equal(t, w.Body.String(), "hola!{\"error\":\"this is an error\"}\n") + assert.Equal(t, 500, w.Code) + assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String()) } func TestSkippingPaths(t *testing.T) { diff --git a/middleware_test.go b/middleware_test.go index 3101d523..273d003d 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -245,6 +245,6 @@ func TestMiddlewareWrite(t *testing.T) { w := performRequest(router, "GET", "/") - assert.Equal(t, w.Code, 400) - assert.Equal(t, strings.Replace(w.Body.String(), " ", "", -1), strings.Replace("hola\nbar{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1)) + assert.Equal(t, 400, w.Code) + assert.Equal(t, strings.Replace("hola\nbar{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1)) } diff --git a/render/render_test.go b/render/render_test.go index 7a6ffb7d..c814ff6d 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -25,8 +25,8 @@ func TestRenderJSON(t *testing.T) { err := (JSON{data}).Render(w) assert.NoError(t, err) - assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") - assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") + assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } func TestRenderIndentedJSON(t *testing.T) { From d158ef2e82cf74c4144e7e709635e1fe2fb260c9 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Wed, 21 Dec 2016 14:24:01 +0800 Subject: [PATCH 14/36] Support disable console color. Signed-off-by: Bo-Yi Wu --- README.md | 3 +++ examples/basic/main.go | 2 ++ logger.go | 23 ++++++++++++++--------- logger_test.go | 7 +++++++ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b215d3f8..e48da263 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,9 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 ```go func main() { + // Disable Console Color + // gin.DisableConsoleColor() + // Creates a gin router with default middleware: // logger and recovery (crash-free) middleware router := gin.Default() diff --git a/examples/basic/main.go b/examples/basic/main.go index 80f2bd3c..984c06ab 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -7,6 +7,8 @@ import ( var DB = make(map[string]string) func main() { + // Disable Console Color + // gin.DisableConsoleColor() r := gin.Default() // Ping test diff --git a/logger.go b/logger.go index 81904d2b..186e3059 100644 --- a/logger.go +++ b/logger.go @@ -14,16 +14,21 @@ import ( ) var ( - green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) - white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) - yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) - red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) - blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) - magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) - cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) - reset = string([]byte{27, 91, 48, 109}) + green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) + white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) + yellow = string([]byte{27, 91, 57, 55, 59, 52, 51, 109}) + red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) + blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) + magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) + cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) + reset = string([]byte{27, 91, 48, 109}) + disableColor = false ) +func DisableConsoleColor() { + disableColor = true +} + func ErrorLogger() HandlerFunc { return ErrorLoggerT(ErrorTypeAny) } @@ -49,7 +54,7 @@ func Logger() HandlerFunc { func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { isTerm := true - if w, ok := out.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) { + if w, ok := out.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) || disableColor { isTerm = false } diff --git a/logger_test.go b/logger_test.go index 7c064c90..6adf86bd 100644 --- a/logger_test.go +++ b/logger_test.go @@ -132,3 +132,10 @@ func TestSkippingPaths(t *testing.T) { performRequest(router, "GET", "/skipped") assert.Contains(t, buffer.String(), "") } + +func TestDisableConsoleColor(t *testing.T) { + New() + assert.False(t, disableColor) + DisableConsoleColor() + assert.True(t, disableColor) +} From 93e36404a1d71ee738c9d5da1ea9a990621baf89 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Fri, 23 Dec 2016 09:12:13 +0800 Subject: [PATCH 15/36] Improve document for #742 Signed-off-by: Bo-Yi Wu --- README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e48da263..e0307ea6 100644 --- a/README.md +++ b/README.md @@ -374,8 +374,41 @@ 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 c.Bind(&person) == nil { + log.Println(person.Name) + log.Println(person.Address) + } + + c.String(200, "Success") +} +``` + + +### Multipart/Urlencoded binding -###Multipart/Urlencoded binding ```go package main From 5cc3d5955f5a03c13daa878c5ef28c33d9a18800 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sat, 24 Dec 2016 12:25:01 +0800 Subject: [PATCH 16/36] Support upload single or multiple files. Signed-off-by: Bo-Yi Wu --- context.go | 20 ++++++++++++++++++-- context_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/context.go b/context.go index d2b98b65..0beeef91 100644 --- a/context.go +++ b/context.go @@ -8,6 +8,7 @@ import ( "errors" "io" "math" + "mime/multipart" "net" "net/http" "net/url" @@ -30,7 +31,10 @@ const ( MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm ) -const abortIndex int8 = math.MaxInt8 / 2 +const ( + defaultMemory = 32 << 20 // 32 MB + abortIndex int8 = math.MaxInt8 / 2 +) // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. @@ -291,7 +295,7 @@ func (c *Context) PostFormArray(key string) []string { func (c *Context) GetPostFormArray(key string) ([]string, bool) { req := c.Request req.ParseForm() - req.ParseMultipartForm(32 << 20) // 32 MB + req.ParseMultipartForm(defaultMemory) if values := req.PostForm[key]; len(values) > 0 { return values, true } @@ -303,6 +307,18 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) { return []string{}, false } +// FormFile returns the first file for the provided form key. +func (c *Context) FormFile(name string) (*multipart.FileHeader, error) { + _, fh, err := c.Request.FormFile(name) + return fh, err +} + +// MultipartForm is the parsed multipart form, including file uploads. +func (c *Context) MultipartForm() (*multipart.Form, error) { + err := c.Request.ParseMultipartForm(defaultMemory) + return c.Request.MultipartForm, err +} + // Bind checks the Content-Type to select a binding engine automatically, // Depending the "Content-Type" header different bindings are used: // "application/json" --> JSON binding diff --git a/context_test.go b/context_test.go index 81e18841..e488f423 100644 --- a/context_test.go +++ b/context_test.go @@ -53,6 +53,37 @@ func must(err error) { } } +func TestContextFormFile(t *testing.T) { + buf := new(bytes.Buffer) + mw := multipart.NewWriter(buf) + w, err := mw.CreateFormFile("file", "test") + if assert.NoError(t, err) { + w.Write([]byte("test")) + } + mw.Close() + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", buf) + c.Request.Header.Set("Content-Type", mw.FormDataContentType()) + f, err := c.FormFile("file") + if assert.NoError(t, err) { + assert.Equal(t, "test", f.Filename) + } +} + +func TestContextMultipartForm(t *testing.T) { + buf := new(bytes.Buffer) + mw := multipart.NewWriter(buf) + mw.WriteField("foo", "bar") + mw.Close() + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("POST", "/", buf) + c.Request.Header.Set("Content-Type", mw.FormDataContentType()) + f, err := c.MultipartForm() + if assert.NoError(t, err) { + assert.NotNil(t, f) + } +} + func TestContextReset(t *testing.T) { router := New() c := router.allocateContext() From 713c3697f44fcb1b43291682133fc42d08a15f31 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 1 Jan 2017 11:54:37 +0100 Subject: [PATCH 17/36] Add instructions for pulling latest changes --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index e48da263..536022c0 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,13 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 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 #### Using GET, POST, PUT, PATCH, DELETE and OPTIONS From ebe3580daf97c0a38bd6237c418a303572249d03 Mon Sep 17 00:00:00 2001 From: David Irvine Date: Mon, 2 Jan 2017 03:05:30 -0500 Subject: [PATCH 18/36] Add convenience method to check if websockets required (#779) * Add convenience method to check if websockets required * Add tests * Fix up tests for develop branch --- context.go | 10 ++++++++++ context_test.go | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/context.go b/context.go index 0beeef91..01c7cb4f 100644 --- a/context.go +++ b/context.go @@ -383,6 +383,16 @@ func (c *Context) ContentType() string { return filterFlags(c.requestHeader("Content-Type")) } +// IsWebsocket returns true if the request headers indicate that a websocket +// handshake is being initiated by the client. +func (c *Context) IsWebsocket() bool { + if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") && + strings.ToLower(c.requestHeader("Upgrade")) == "websocket" { + return true + } + return false +} + func (c *Context) requestHeader(key string) string { if values, _ := c.Request.Header[key]; len(values) > 0 { return values[0] diff --git a/context_test.go b/context_test.go index e488f423..fe22c492 100644 --- a/context_test.go +++ b/context_test.go @@ -814,3 +814,25 @@ func TestContextGolangContext(t *testing.T) { assert.Equal(t, c.Value("foo"), "bar") assert.Nil(t, c.Value(1)) } + +func TestWebsocketsRequired(t *testing.T) { + // Example request from spec: https://tools.ietf.org/html/rfc6455#section-1.2 + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("GET", "/chat", nil) + c.Request.Header.Set("Host", "server.example.com") + c.Request.Header.Set("Upgrade", "websocket") + c.Request.Header.Set("Connection", "Upgrade") + c.Request.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") + c.Request.Header.Set("Origin", "http://example.com") + c.Request.Header.Set("Sec-WebSocket-Protocol", "chat, superchat") + c.Request.Header.Set("Sec-WebSocket-Version", "13") + + assert.True(t, c.IsWebsocket()) + + // Normal request, no websocket required. + c, _ = CreateTestContext(httptest.NewRecorder()) + c.Request, _ = http.NewRequest("GET", "/chat", nil) + c.Request.Header.Set("Host", "server.example.com") + + assert.False(t, c.IsWebsocket()) +} From aa1a2b75fafe8fa2d25647da228fda4fa82f8f67 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Mon, 2 Jan 2017 18:01:25 +0800 Subject: [PATCH 19/36] Add comment for the logic behind func. Signed-off-by: Bo-Yi Wu --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 49345648..19d9088b 100644 --- a/README.md +++ b/README.md @@ -404,6 +404,9 @@ func main() { 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) From 6596aa3b715b3bc0f12f8c319e4bad23012c9e11 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 3 Jan 2017 10:34:27 +0800 Subject: [PATCH 20/36] [ci skip] update readme for upload file. Signed-off-by: Bo-Yi Wu --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 19d9088b..7669edf7 100644 --- a/README.md +++ b/README.md @@ -229,34 +229,64 @@ func main() { id: 1234; page: 1; name: manu; message: this_is_great ``` -### Another example: upload file +### upload file -References issue [#548](https://github.com/gin-gonic/gin/issues/548). +#### upload single file + +References issue [#774](https://github.com/gin-gonic/gin/issues/774). ```go func main() { router := gin.Default() - router.POST("/upload", func(c *gin.Context) { + // single file + file, _ := c.FormFile("file") + log.Println(file.Filename) - file, header , err := c.Request.FormFile("upload") - filename := header.Filename - fmt.Println(header.Filename) - out, err := os.Create("./tmp/"+filename+".png") - if err != nil { - log.Fatal(err) - } - defer out.Close() - _, err = io.Copy(out, file) - if err != nil { - log.Fatal(err) - } + c.String(http.StatusOK, "Uploaded...") }) router.Run(":8080") } ``` +curl command: + +```bash +curl -X POST http://localhost:8080/upload \ + -F "file=@/Users/appleboy/test.zip" \ + -H "Content-Type: multipart/form-data" +``` + +#### upload multiple files + +```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, "Uploaded...") + }) + router.Run(":8080") +} +``` + +curl command: + +```bash +curl -X POST http://localhost:8080/upload \ + -F "upload[]=@/Users/appleboy/test1.zip" \ + -F "upload[]=@/Users/appleboy/test2.zip" \ + -H "Content-Type: multipart/form-data" +``` + #### Grouping routes + ```go func main() { router := gin.Default() From 17af53a565bff4483c734fe00f1870a4ecf05afc Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 3 Jan 2017 11:50:35 +0800 Subject: [PATCH 21/36] add upload file example. Signed-off-by: Bo-Yi Wu --- README.md | 4 +- examples/upload-file/multiple/main.go | 39 +++++++++++++++++++ .../upload-file/multiple/public/index.html | 17 ++++++++ examples/upload-file/single/main.go | 34 ++++++++++++++++ examples/upload-file/single/public/index.html | 16 ++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 examples/upload-file/multiple/main.go create mode 100644 examples/upload-file/multiple/public/index.html create mode 100644 examples/upload-file/single/main.go create mode 100644 examples/upload-file/single/public/index.html diff --git a/README.md b/README.md index 7669edf7..571c7ce7 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ id: 1234; page: 1; name: manu; message: this_is_great #### upload single file -References issue [#774](https://github.com/gin-gonic/gin/issues/774). +References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single). ```go func main() { @@ -259,6 +259,8 @@ curl -X POST http://localhost:8080/upload \ #### upload multiple files +See the detail [example code](examples/upload-file/multiple). + ```go func main() { router := gin.Default() diff --git a/examples/upload-file/multiple/main.go b/examples/upload-file/multiple/main.go new file mode 100644 index 00000000..2f4e9b52 --- /dev/null +++ b/examples/upload-file/multiple/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.Static("/", "./public") + router.POST("/upload", func(c *gin.Context) { + name := c.PostForm("name") + email := c.PostForm("email") + + // Multipart form + form, _ := c.MultipartForm() + files := form.File["files"] + + for _, file := range files { + // Source + src, _ := file.Open() + defer src.Close() + + // Destination + dst, _ := os.Create(file.Filename) + defer dst.Close() + + // Copy + io.Copy(dst, src) + } + + c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email)) + }) + router.Run(":8080") +} diff --git a/examples/upload-file/multiple/public/index.html b/examples/upload-file/multiple/public/index.html new file mode 100644 index 00000000..b8463601 --- /dev/null +++ b/examples/upload-file/multiple/public/index.html @@ -0,0 +1,17 @@ + + + + + Multiple file upload + + +

Upload multiple files with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ + diff --git a/examples/upload-file/single/main.go b/examples/upload-file/single/main.go new file mode 100644 index 00000000..9acf5009 --- /dev/null +++ b/examples/upload-file/single/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.Static("/", "./public") + router.POST("/upload", func(c *gin.Context) { + name := c.PostForm("name") + email := c.PostForm("email") + + // Source + file, _ := c.FormFile("file") + src, _ := file.Open() + defer src.Close() + + // Destination + dst, _ := os.Create(file.Filename) + defer dst.Close() + + // Copy + io.Copy(dst, src) + + c.String(http.StatusOK, fmt.Sprintf("File %s uploaded successfully with fields name=%s and email=%s.", file.Filename, name, email)) + }) + router.Run(":8080") +} diff --git a/examples/upload-file/single/public/index.html b/examples/upload-file/single/public/index.html new file mode 100644 index 00000000..b0c2a808 --- /dev/null +++ b/examples/upload-file/single/public/index.html @@ -0,0 +1,16 @@ + + + + + Single file upload + + +

Upload single file with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ From 970e96e38806c3636819dcc6b3df4ff7ecf382b3 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Tue, 3 Jan 2017 23:42:21 +0800 Subject: [PATCH 22/36] test: update client ip testing. --- context_test.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/context_test.go b/context_test.go index fe22c492..7dc3085b 100644 --- a/context_test.go +++ b/context_test.go @@ -718,24 +718,25 @@ func TestContextClientIP(t *testing.T) { c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50") c.Request.RemoteAddr = " 40.40.40.40:42123 " - assert.Equal(t, c.ClientIP(), "10.10.10.10") - - c.Request.Header.Del("X-Real-IP") - assert.Equal(t, c.ClientIP(), "20.20.20.20") - - c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ") - assert.Equal(t, c.ClientIP(), "30.30.30.30") + assert.Equal(t, "20.20.20.20", c.ClientIP()) c.Request.Header.Del("X-Forwarded-For") + assert.Equal(t, "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, c.ClientIP(), "50.50.50.50") + assert.Equal(t, "50.50.50.50", c.ClientIP()) c.Request.Header.Del("X-Appengine-Remote-Addr") - assert.Equal(t, c.ClientIP(), "40.40.40.40") + assert.Equal(t, "40.40.40.40", c.ClientIP()) // no port c.Request.RemoteAddr = "50.50.50.50" - assert.Equal(t, c.ClientIP(), "") + assert.Equal(t, "", c.ClientIP()) } func TestContextContentType(t *testing.T) { From c115074d773cb135f8c647992e792b91ad3bb3d9 Mon Sep 17 00:00:00 2001 From: tsirolnik Date: Tue, 30 Aug 2016 18:58:39 +0300 Subject: [PATCH 23/36] Use X-Forwarded-For before X-Real-Ip Nginx uses X-Real-Ip with its IP instead of the client's IP. Therefore, we should use X-Forwarded-For *before* X-Real-Ip --- context.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/context.go b/context.go index 01c7cb4f..d19e1ee9 100644 --- a/context.go +++ b/context.go @@ -349,13 +349,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 // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. +// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP. func (c *Context) ClientIP() string { if c.engine.ForwardedByClientIP { - clientIP := strings.TrimSpace(c.requestHeader("X-Real-Ip")) - if len(clientIP) > 0 { - return clientIP - } - clientIP = c.requestHeader("X-Forwarded-For") + clientIP := c.requestHeader("X-Forwarded-For") if index := strings.IndexByte(clientIP, ','); index >= 0 { clientIP = clientIP[0:index] } @@ -363,6 +360,10 @@ func (c *Context) ClientIP() string { if len(clientIP) > 0 { return clientIP } + clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip")) + if len(clientIP) > 0 { + return clientIP + } } if c.engine.AppEngine { From 050937dab80745a7fc666e5aa0b61f76169c48dc Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Tue, 3 Jan 2017 17:04:29 +0100 Subject: [PATCH 24/36] Improve "Upload file" example, Fix MD typos --- README.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 571c7ce7..0c08556f 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 ## API Examples -#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS +### Using GET, POST, PUT, PATCH, DELETE and OPTIONS ```go func main() { @@ -137,7 +137,7 @@ func main() { } ``` -#### Parameters in path +### Parameters in path ```go func main() { @@ -162,7 +162,7 @@ func main() { } ``` -#### Querystring parameters +### Querystring parameters ```go func main() { router := gin.Default() @@ -229,9 +229,9 @@ func main() { id: 1234; page: 1; name: manu; message: this_is_great ``` -### upload file +### Upload files -#### upload single file +#### Single file References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single). @@ -243,13 +243,13 @@ func main() { file, _ := c.FormFile("file") log.Println(file.Filename) - c.String(http.StatusOK, "Uploaded...") + c.String(http.StatusOK, fmt.Printf("'%s' uploaded!", file.Filename)) }) router.Run(":8080") } ``` -curl command: +How to `curl`: ```bash curl -X POST http://localhost:8080/upload \ @@ -257,7 +257,7 @@ curl -X POST http://localhost:8080/upload \ -H "Content-Type: multipart/form-data" ``` -#### upload multiple files +#### Multiple files See the detail [example code](examples/upload-file/multiple). @@ -272,13 +272,13 @@ func main() { for _, file := range files { log.Println(file.Filename) } - c.String(http.StatusOK, "Uploaded...") + c.String(http.StatusOK, fmt.Printf("%d files uploaded!", len(files))) }) router.Run(":8080") } ``` -curl command: +How to `curl`: ```bash curl -X POST http://localhost:8080/upload \ @@ -287,7 +287,7 @@ curl -X POST http://localhost:8080/upload \ -H "Content-Type: multipart/form-data" ``` -#### Grouping routes +### Grouping routes ```go func main() { @@ -314,7 +314,7 @@ func main() { ``` -#### Blank Gin without middleware by default +### Blank Gin without middleware by default Use @@ -328,7 +328,7 @@ r := gin.Default() ``` -#### Using middleware +### Using middleware ```go func main() { // Creates a router without any middleware by default @@ -363,7 +363,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). @@ -413,7 +413,7 @@ func main() { } ``` -#### Bind Query String +### Bind Query String See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292). @@ -489,7 +489,7 @@ $ curl -v --form user=user --form password=password http://localhost:8080/login ``` -#### XML, JSON and YAML rendering +### XML, JSON and YAML rendering ```go func main() { @@ -528,7 +528,7 @@ func main() { } ``` -####Serving static files +### Serving static files ```go func main() { @@ -542,7 +542,7 @@ func main() { } ``` -####HTML rendering +### HTML rendering Using LoadHTMLGlob() or LoadHTMLFiles() @@ -622,7 +622,7 @@ func main() { ``` -#### Redirects +### Redirects Issuing a HTTP redirect is easy: @@ -634,7 +634,7 @@ r.GET("/test", func(c *gin.Context) { Both internal and external locations are supported. -#### Custom Middleware +### Custom Middleware ```go func Logger() gin.HandlerFunc { @@ -674,7 +674,7 @@ func main() { } ``` -#### Using BasicAuth() middleware +### Using BasicAuth() middleware ```go // simulate some private data var secrets = gin.H{ @@ -713,7 +713,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. ```go @@ -745,7 +745,7 @@ func main() { } ``` -#### Custom HTTP configuration +### Custom HTTP configuration Use `http.ListenAndServe()` directly, like this: @@ -772,7 +772,7 @@ func main() { } ``` -#### Graceful restart or stop +### Graceful restart or stop Do you want to graceful restart or stop your web server? There are some ways this can be done. @@ -803,7 +803,7 @@ An alternative to endless: - You should add/modify tests to cover your proposed code changes. - If your pull request contains a new feature, please document it on the README. -## Example +## Users Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. From 8191cdf5d6ec3a5430c4599448b50491683c6a71 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Thu, 5 Jan 2017 12:22:32 +0800 Subject: [PATCH 25/36] docs: update readme for multiple template package. (#786) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0c08556f..c940a107 100644 --- a/README.md +++ b/README.md @@ -621,6 +621,9 @@ func main() { } ``` +### Multitemplate + +Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`. ### Redirects From 75c2274b4b3c5dc47f11767324d7aefb655011d5 Mon Sep 17 00:00:00 2001 From: novaeye Date: Thu, 5 Jan 2017 16:29:33 +0800 Subject: [PATCH 26/36] better display for log message (#623) --- logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.go b/logger.go index 186e3059..c7cbfe1f 100644 --- a/logger.go +++ b/logger.go @@ -92,7 +92,7 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc { } comment := c.Errors.ByType(ErrorTypePrivate).String() - fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s", + fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %s %-7s %s\n%s", end.Format("2006/01/02 - 15:04:05"), statusColor, statusCode, reset, latency, From 963acc4b0ce297782405d4aefd6fe173ff657b1f Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Mon, 9 Jan 2017 16:24:48 +0100 Subject: [PATCH 27/36] Fix #198 (#781) * Add new function to Render interface for writing content type only * Add support for the new function in Render interface for writing content-type only * Fix unhandled merge conflict in context_test.go * Update vendor.json --- context.go | 30 ++++++++--- context_test.go | 132 ++++++++++++++++++++++++++++++++++++++++++++- middleware_test.go | 2 +- render/data.go | 15 +++--- render/html.go | 7 ++- render/json.go | 29 ++++++---- render/redirect.go | 2 + render/render.go | 1 + render/text.go | 5 +- render/xml.go | 6 ++- render/yaml.go | 6 ++- vendor/vendor.json | 12 ++--- 12 files changed, 213 insertions(+), 34 deletions(-) diff --git a/context.go b/context.go index d19e1ee9..e937a4c7 100644 --- a/context.go +++ b/context.go @@ -17,7 +17,7 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" - "github.com/manucorporat/sse" + "gopkg.in/gin-contrib/sse.v0" ) // Content-Type MIME of the most common data formats @@ -405,6 +405,19 @@ func (c *Context) requestHeader(key string) string { /******** RESPONSE RENDERING ********/ /************************************/ +// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function +func bodyAllowedForStatus(status int) bool { + switch { + case status >= 100 && status <= 199: + return false + case status == 204: + return false + case status == 304: + return false + } + return true +} + func (c *Context) Status(code int) { c.writermem.WriteHeader(code) } @@ -454,6 +467,13 @@ func (c *Context) Cookie(name string) (string, error) { func (c *Context) Render(code int, r render.Render) { c.Status(code) + + if !bodyAllowedForStatus(code) { + r.WriteContentType(c.Writer) + c.Writer.WriteHeaderNow() + return + } + if err := r.Render(c.Writer); err != nil { panic(err) } @@ -478,10 +498,7 @@ func (c *Context) IndentedJSON(code int, obj interface{}) { // JSON serializes the given struct as JSON into the response body. // It also sets the Content-Type as "application/json". func (c *Context) JSON(code int, obj interface{}) { - c.Status(code) - if err := render.WriteJSON(c.Writer, obj); err != nil { - panic(err) - } + c.Render(code, render.JSON{Data: obj}) } // XML serializes the given struct as XML into the response body. @@ -497,8 +514,7 @@ func (c *Context) YAML(code int, obj interface{}) { // String writes the given string into the response body. func (c *Context) String(code int, format string, values ...interface{}) { - c.Status(code) - render.WriteString(c.Writer, format, values) + c.Render(code, render.String{Format: format, Data: values}) } // Redirect returns a HTTP redirect to the specific location. diff --git a/context_test.go b/context_test.go index 7dc3085b..fd8ca4a2 100644 --- a/context_test.go +++ b/context_test.go @@ -7,6 +7,7 @@ package gin import ( "bytes" "errors" + "fmt" "html/template" "mime/multipart" "net/http" @@ -15,9 +16,9 @@ import ( "testing" "time" - "github.com/manucorporat/sse" "github.com/stretchr/testify/assert" "golang.org/x/net/context" + "gopkg.in/gin-contrib/sse.v0" ) var _ context.Context = &Context{} @@ -381,6 +382,35 @@ func TestContextGetCookie(t *testing.T) { 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 // and Content-Type is set to application/json func TestContextRenderJSON(t *testing.T) { @@ -394,6 +424,18 @@ func TestContextRenderJSON(t *testing.T) { assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) } +// Tests that no JSON is rendered if code is 204 +func TestContextRenderNoContentJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.JSON(204, H{"foo": "bar"}) + + assert.Equal(t, 204, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) +} + // Tests that the response is serialized as JSON // we change the content-type before func TestContextRenderAPIJSON(t *testing.T) { @@ -408,6 +450,19 @@ func TestContextRenderAPIJSON(t *testing.T) { assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type")) } +// Tests that no Custom JSON is rendered if code is 204 +func TestContextRenderNoContentAPIJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Header("Content-Type", "application/vnd.api+json") + c.JSON(204, H{"foo": "bar"}) + + assert.Equal(t, 204, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json") +} + // Tests that the response is serialized as JSON // and Content-Type is set to application/json func TestContextRenderIndentedJSON(t *testing.T) { @@ -421,6 +476,18 @@ func TestContextRenderIndentedJSON(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") } +// Tests that no Custom JSON is rendered if code is 204 +func TestContextRenderNoContentIndentedJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) + + assert.Equal(t, 204, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") +} + // Tests that the response executes the templates // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { @@ -436,6 +503,20 @@ func TestContextRenderHTML(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } +// Tests that no HTML is rendered if code is 204 +func TestContextRenderNoContentHTML(t *testing.T) { + w := httptest.NewRecorder() + c, router := CreateTestContext(w) + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) + router.SetHTMLTemplate(templ) + + c.HTML(204, "t", H{"name": "alexandernyquist"}) + + assert.Equal(t, 204, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") +} + // TestContextXML tests that the response is serialized as XML // and Content-Type is set to application/xml func TestContextRenderXML(t *testing.T) { @@ -449,6 +530,18 @@ func TestContextRenderXML(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") } +// Tests that no XML is rendered if code is 204 +func TestContextRenderNoContentXML(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.XML(204, H{"foo": "bar"}) + + assert.Equal(t, 204, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") +} + // TestContextString tests that the response is returned // with Content-Type set to text/plain func TestContextRenderString(t *testing.T) { @@ -462,6 +555,18 @@ func TestContextRenderString(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") } +// Tests that no String is rendered if code is 204 +func TestContextRenderNoContentString(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.String(204, "test %s %d", "string", 2) + + assert.Equal(t, 204, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") +} + // TestContextString tests that the response is returned // with Content-Type set to text/html func TestContextRenderHTMLString(t *testing.T) { @@ -476,6 +581,19 @@ func TestContextRenderHTMLString(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } +// Tests that no HTML String is rendered if code is 204 +func TestContextRenderNoContentHTMLString(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(204, "%s %d", "string", 3) + + assert.Equal(t, 204, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") +} + // TestContextData tests that the response can be written from `bytesting` // with specified MIME type func TestContextRenderData(t *testing.T) { @@ -489,6 +607,18 @@ func TestContextRenderData(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") } +// Tests that no Custom Data is rendered if code is 204 +func TestContextRenderNoContentData(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Data(204, "text/csv", []byte(`foo,bar`)) + + assert.Equal(t, 204, w.Code) + assert.Equal(t, "", w.Body.String()) + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") +} + func TestContextRenderSSE(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) diff --git a/middleware_test.go b/middleware_test.go index 273d003d..c77f8276 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -10,8 +10,8 @@ import ( "testing" - "github.com/manucorporat/sse" "github.com/stretchr/testify/assert" + "gopkg.in/gin-contrib/sse.v0" ) func TestMiddlewareGeneralCase(t *testing.T) { diff --git a/render/data.go b/render/data.go index efa75d55..c296042c 100644 --- a/render/data.go +++ b/render/data.go @@ -11,10 +11,13 @@ type Data struct { Data []byte } -func (r Data) Render(w http.ResponseWriter) error { - if len(r.ContentType) > 0 { - w.Header()["Content-Type"] = []string{r.ContentType} - } - w.Write(r.Data) - return nil +// Render (Data) writes data with custom ContentType +func (r Data) Render(w http.ResponseWriter) (err error) { + r.WriteContentType(w) + _, err = w.Write(r.Data) + return +} + +func (r Data) WriteContentType(w http.ResponseWriter) { + writeContentType(w, []string{r.ContentType}) } diff --git a/render/html.go b/render/html.go index 8bfb23ac..a3cbda67 100644 --- a/render/html.go +++ b/render/html.go @@ -58,9 +58,14 @@ func (r HTMLDebug) loadTemplate() *template.Template { } func (r HTML) Render(w http.ResponseWriter) error { - writeContentType(w, htmlContentType) + r.WriteContentType(w) + if len(r.Name) == 0 { return r.Template.Execute(w, r.Data) } return r.Template.ExecuteTemplate(w, r.Name, r.Data) } + +func (r HTML) WriteContentType(w http.ResponseWriter) { + writeContentType(w, htmlContentType) +} diff --git a/render/json.go b/render/json.go index b652c4c8..3ee8b132 100644 --- a/render/json.go +++ b/render/json.go @@ -21,18 +21,15 @@ type ( var jsonContentType = []string{"application/json; charset=utf-8"} -func (r JSON) Render(w http.ResponseWriter) error { - return WriteJSON(w, r.Data) +func (r JSON) Render(w http.ResponseWriter) (err error) { + if err = WriteJSON(w, r.Data); err != nil { + panic(err) + } + return } -func (r IndentedJSON) Render(w http.ResponseWriter) error { +func (r JSON) WriteContentType(w http.ResponseWriter) { writeContentType(w, jsonContentType) - jsonBytes, err := json.MarshalIndent(r.Data, "", " ") - if err != nil { - return err - } - w.Write(jsonBytes) - return nil } func WriteJSON(w http.ResponseWriter, obj interface{}) error { @@ -44,3 +41,17 @@ func WriteJSON(w http.ResponseWriter, obj interface{}) error { w.Write(jsonBytes) return nil } + +func (r IndentedJSON) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + jsonBytes, err := json.MarshalIndent(r.Data, "", " ") + if err != nil { + return err + } + w.Write(jsonBytes) + return nil +} + +func (r IndentedJSON) WriteContentType(w http.ResponseWriter) { + writeContentType(w, jsonContentType) +} diff --git a/render/redirect.go b/render/redirect.go index bd48d7d8..f874a351 100644 --- a/render/redirect.go +++ b/render/redirect.go @@ -22,3 +22,5 @@ func (r Redirect) Render(w http.ResponseWriter) error { http.Redirect(w, r.Request, r.Location, r.Code) return nil } + +func (r Redirect) WriteContentType(http.ResponseWriter) {} diff --git a/render/render.go b/render/render.go index 3808666a..7e997374 100644 --- a/render/render.go +++ b/render/render.go @@ -8,6 +8,7 @@ import "net/http" type Render interface { Render(http.ResponseWriter) error + WriteContentType(w http.ResponseWriter) } var ( diff --git a/render/text.go b/render/text.go index 5a9e280b..74cd26be 100644 --- a/render/text.go +++ b/render/text.go @@ -22,9 +22,12 @@ func (r String) Render(w http.ResponseWriter) error { return nil } +func (r String) WriteContentType(w http.ResponseWriter) { + writeContentType(w, plainContentType) +} + func WriteString(w http.ResponseWriter, format string, data []interface{}) { writeContentType(w, plainContentType) - if len(data) > 0 { fmt.Fprintf(w, format, data...) } else { diff --git a/render/xml.go b/render/xml.go index be22e6f2..cff1ac3e 100644 --- a/render/xml.go +++ b/render/xml.go @@ -16,6 +16,10 @@ type XML struct { var xmlContentType = []string{"application/xml; charset=utf-8"} func (r XML) Render(w http.ResponseWriter) error { - writeContentType(w, xmlContentType) + r.WriteContentType(w) return xml.NewEncoder(w).Encode(r.Data) } + +func (r XML) WriteContentType(w http.ResponseWriter) { + writeContentType(w, xmlContentType) +} diff --git a/render/yaml.go b/render/yaml.go index 46937d88..25d0ebd4 100644 --- a/render/yaml.go +++ b/render/yaml.go @@ -17,7 +17,7 @@ type YAML struct { var yamlContentType = []string{"application/x-yaml; charset=utf-8"} func (r YAML) Render(w http.ResponseWriter) error { - writeContentType(w, yamlContentType) + r.WriteContentType(w) bytes, err := yaml.Marshal(r.Data) if err != nil { @@ -27,3 +27,7 @@ func (r YAML) Render(w http.ResponseWriter) error { w.Write(bytes) return nil } + +func (r YAML) WriteContentType(w http.ResponseWriter) { + writeContentType(w, yamlContentType) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 3c6b0edb..e754a3fd 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -21,12 +21,6 @@ "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", "revisionTime": "2016-11-17T03:31:26Z" }, - { - "checksumSHA1": "b0T0Hzd+zYk+OCDTFMps+jwa/nY=", - "path": "github.com/manucorporat/sse", - "revision": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d", - "revisionTime": "2016-01-26T18:01:36Z" - }, { "checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=", "path": "github.com/manucorporat/stats", @@ -66,6 +60,12 @@ "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", From 2d8477fc427b4864194c0221a1c16b2659d8207f Mon Sep 17 00:00:00 2001 From: mbesancon Date: Wed, 1 Feb 2017 15:47:50 +0100 Subject: [PATCH 28/36] Fixed typos in Context (#797) Simple english typos in the Copy() method --- context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context.go b/context.go index e937a4c7..601754fe 100644 --- a/context.go +++ b/context.go @@ -68,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. -// This have to be used then the context has to be passed to a goroutine. +// This has to be used when the context has to be passed to a goroutine. func (c *Context) Copy() *Context { var cp = *c cp.writermem.ResponseWriter = nil From 049da60f5114a479f04a668f3d8a6f3b44fe6658 Mon Sep 17 00:00:00 2001 From: Vasilyuk Vasiliy Date: Wed, 1 Feb 2017 18:49:24 +0400 Subject: [PATCH 29/36] Update gin version (#790) * Revert "Merge pull request #753 from gin-gonic/bug" This reverts commit 556287ff0856a5ad1f9a1b493c188cabeceba929, reversing changes made to 32cab500ecc71d2975f5699c8a65c6debb29cfbe. * Revert "Merge pull request #744 from aviddiviner/logger-fix" This reverts commit c3bfd69303d0fdaf2d43a7ff07cc8ee45ec7bb3f, reversing changes made to 9177f01c2843b91820780197f521ba48554b9df3. * Update gin version From 2ae8a25fc93067526eaa8d6cdf2b9a276a044986 Mon Sep 17 00:00:00 2001 From: Franz Bettag Date: Fri, 10 Feb 2017 15:44:55 +0100 Subject: [PATCH 30/36] Added HandleContext to re-enter soft-rewritten Requests. (#709) --- gin.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gin.go b/gin.go index d084f2a3..12054090 100644 --- a/gin.go +++ b/gin.go @@ -273,6 +273,15 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { engine.pool.Put(c) } +// Re-enter a context that has been rewritten. +// This can be done by setting c.Request.Path to your new target. +// Disclaimer: You can loop yourself to death with this, use wisely. +func (engine *Engine) HandleContext(c *Context) { + c.reset() + engine.handleHTTPRequest(c) + engine.pool.Put(c) +} + func (engine *Engine) handleHTTPRequest(context *Context) { httpMethod := context.Request.Method path := context.Request.URL.Path From 6ce1e86a2715573af0c93ac2c4e4bda8788a0ad9 Mon Sep 17 00:00:00 2001 From: pjgg Date: Tue, 14 Feb 2017 02:11:01 +0100 Subject: [PATCH 31/36] chore(errorHandler):new abortWithStatus method with Json body (#800) --- context.go | 7 +++++++ context_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/context.go b/context.go index 601754fe..b4f23952 100644 --- a/context.go +++ b/context.go @@ -120,6 +120,13 @@ func (c *Context) AbortWithStatus(code int) { c.Abort() } +// AbortWithStatusJSON calls `Abort()` and then `JSON` internally. This method stops the chain, writes the status code and return a JSON body +// It also sets the Content-Type as "application/json". +func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) { + c.Abort() + c.JSON(code, jsonObj) +} + // AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and // pushes the specified error to `c.Errors`. // See Context.Error() for more details. diff --git a/context_test.go b/context_test.go index fd8ca4a2..8d59f6e3 100644 --- a/context_test.go +++ b/context_test.go @@ -788,6 +788,36 @@ func TestContextAbortWithStatus(t *testing.T) { assert.True(t, c.IsAborted()) } +type testJSONAbortMsg struct { + Foo string `json:"foo"` + Bar string `json:"bar"` +} + +func TestContextAbortWithStatusJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.index = 4 + + in := new(testJSONAbortMsg) + in.Bar = "barValue" + in.Foo = "fooValue" + + c.AbortWithStatusJSON(415, in) + + assert.Equal(t, c.index, abortIndex) + assert.Equal(t, c.Writer.Status(), 415) + assert.Equal(t, w.Code, 415) + assert.True(t, c.IsAborted()) + + contentType := w.Header().Get("Content-Type") + assert.Equal(t, contentType, "application/json; charset=utf-8") + + buf := new(bytes.Buffer) + buf.ReadFrom(w.Body) + jsonStringBody := buf.String() + assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody) +} + func TestContextError(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) assert.Empty(t, c.Errors) From 7fcc6088c1756f28c0ad4f21c4f7363253df6b7f Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Thu, 16 Feb 2017 09:27:10 +0800 Subject: [PATCH 32/36] Support testing on latest version of golang. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e6a05bd0..1e5976fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go sudo: false go: - - 1.6.4 - - 1.7.4 + - 1.6.x + - 1.7.x - tip git: From d94ee48fb7a4e7afd822ffc77d1d62a3f69fc18b Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Fri, 17 Feb 2017 16:38:56 +0800 Subject: [PATCH 33/36] Add go 1.8.x testing on travis (#806) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1e5976fa..9fb59940 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ sudo: false go: - 1.6.x - 1.7.x + - 1.8.x - tip git: From 863248034b46c8292bf53681730fa033292710fa Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Fri, 17 Feb 2017 11:32:36 -0200 Subject: [PATCH 34/36] Support time.Time on form binding (#801) --- binding/form_mapping.go | 27 +++++++++++++++++++++++++++ context_test.go | 20 +++++++++++++++----- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 07c83751..bc9e44cf 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -8,6 +8,7 @@ import ( "errors" "reflect" "strconv" + "time" ) func mapForm(ptr interface{}, form map[string][]string) error { @@ -52,6 +53,12 @@ func mapForm(ptr interface{}, form map[string][]string) error { } val.Field(i).Set(slice) } else { + if _, isTime := structField.Interface().(time.Time); isTime { + if err := setTimeField(inputValue[0], typeField, structField); err != nil { + return err + } + continue + } if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { return err } @@ -140,6 +147,26 @@ func setFloatField(val string, bitSize int, field reflect.Value) error { return err } +func setTimeField(val string, structField reflect.StructField, value reflect.Value) error { + timeFormat := structField.Tag.Get("time_format") + if timeFormat == "" { + return errors.New("Blank time format") + } + + l := time.Local + if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC { + l = time.UTC + } + + t, err := time.ParseInLocation(timeFormat, val, l) + if err != nil { + return err + } + + value.Set(reflect.ValueOf(t)) + return nil +} + // Don't pass in pointers to bind to. Can lead to bugs. See: // https://github.com/codegangsta/martini-contrib/issues/40 // https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 diff --git a/context_test.go b/context_test.go index 8d59f6e3..ebc5050e 100644 --- a/context_test.go +++ b/context_test.go @@ -42,6 +42,8 @@ func createMultipartRequest() *http.Request { must(mw.WriteField("array", "first")) must(mw.WriteField("array", "second")) must(mw.WriteField("id", "")) + must(mw.WriteField("time_local", "31/12/2016 14:55")) + must(mw.WriteField("time_utc", "31/12/2016 14:55")) req, err := http.NewRequest("POST", "/", body) must(err) req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) @@ -309,11 +311,14 @@ func TestContextPostFormMultipart(t *testing.T) { c.Request = createMultipartRequest() var obj struct { - Foo string `form:"foo"` - Bar string `form:"bar"` - BarAsInt int `form:"bar"` - Array []string `form:"array"` - ID string `form:"id"` + Foo string `form:"foo"` + Bar string `form:"bar"` + BarAsInt int `form:"bar"` + Array []string `form:"array"` + ID string `form:"id"` + TimeLocal time.Time `form:"time_local" time_format:"02/01/2006 15:04"` + TimeUTC time.Time `form:"time_utc" time_format:"02/01/2006 15:04" time_utc:"1"` + BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"` } assert.NoError(t, c.Bind(&obj)) assert.Equal(t, obj.Foo, "bar") @@ -321,6 +326,11 @@ func TestContextPostFormMultipart(t *testing.T) { assert.Equal(t, obj.BarAsInt, 10) assert.Equal(t, obj.Array, []string{"first", "second"}) assert.Equal(t, obj.ID, "") + assert.Equal(t, obj.TimeLocal.Format("02/01/2006 15:04"), "31/12/2016 14:55") + assert.Equal(t, obj.TimeLocal.Location(), time.Local) + assert.Equal(t, obj.TimeUTC.Format("02/01/2006 15:04"), "31/12/2016 14:55") + assert.Equal(t, obj.TimeUTC.Location(), time.UTC) + assert.True(t, obj.BlankTime.IsZero()) value, ok := c.GetQuery("foo") assert.False(t, ok) From 5be2123c1a4407f41062d33500db4de65e05bab1 Mon Sep 17 00:00:00 2001 From: Harindu Perera Date: Thu, 23 Feb 2017 15:08:37 +0100 Subject: [PATCH 35/36] Added support for MessagePack binding and rendering (#808) Added deps to vendor.json and fixed rendering bug --- binding/binding.go | 5 +++++ binding/binding_test.go | 45 +++++++++++++++++++++++++++++++++++++++-- binding/msgpack.go | 28 +++++++++++++++++++++++++ render/msgpack.go | 31 ++++++++++++++++++++++++++++ render/render.go | 2 ++ render/render_test.go | 23 +++++++++++++++++++++ vendor/vendor.json | 6 ++++++ 7 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 binding/msgpack.go create mode 100644 render/msgpack.go diff --git a/binding/binding.go b/binding/binding.go index dc7397f1..d3a2c97e 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -15,6 +15,8 @@ const ( MIMEPOSTForm = "application/x-www-form-urlencoded" MIMEMultipartPOSTForm = "multipart/form-data" MIMEPROTOBUF = "application/x-protobuf" + MIMEMSGPACK = "application/x-msgpack" + MIMEMSGPACK2 = "application/msgpack" ) type Binding interface { @@ -40,6 +42,7 @@ var ( FormPost = formPostBinding{} FormMultipart = formMultipartBinding{} ProtoBuf = protobufBinding{} + MsgPack = msgpackBinding{} ) func Default(method, contentType string) Binding { @@ -53,6 +56,8 @@ func Default(method, contentType string) Binding { return XML case MIMEPROTOBUF: return ProtoBuf + case MIMEMSGPACK, MIMEMSGPACK2: + return MsgPack default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: return Form } diff --git a/binding/binding_test.go b/binding/binding_test.go index 72f60152..cf005948 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -12,17 +12,18 @@ import ( "github.com/gin-gonic/gin/binding/example" "github.com/golang/protobuf/proto" + "github.com/ugorji/go/codec" "github.com/stretchr/testify/assert" ) type FooStruct struct { - Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"` + Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"` } type FooBarStruct struct { FooStruct - Bar string `json:"bar" form:"bar" xml:"bar" binding:"required"` + Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"` } func TestBindingDefault(t *testing.T) { @@ -43,6 +44,9 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, Default("POST", MIMEPROTOBUF), ProtoBuf) assert.Equal(t, Default("PUT", MIMEPROTOBUF), ProtoBuf) + + assert.Equal(t, Default("POST", MIMEMSGPACK), MsgPack) + assert.Equal(t, Default("PUT", MIMEMSGPACK2), MsgPack) } func TestBindingJSON(t *testing.T) { @@ -121,6 +125,26 @@ func TestBindingProtoBuf(t *testing.T) { string(data), string(data[1:])) } +func TestBindingMsgPack(t *testing.T) { + test := FooStruct{ + Foo: "bar", + } + + h := new(codec.MsgpackHandle) + assert.NotNil(t, h) + buf := bytes.NewBuffer([]byte{}) + assert.NotNil(t, buf) + err := codec.NewEncoder(buf, h).Encode(test) + assert.NoError(t, err) + + data := buf.Bytes() + + testMsgPackBodyBinding(t, + MsgPack, "msgpack", + "/", "/", + string(data), string(data[1:])) +} + func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody("POST", "/", `{"bar": "foo"}`) @@ -213,6 +237,23 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba assert.Error(t, err) } +func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStruct{} + req := requestWithBody("POST", path, body) + req.Header.Add("Content-Type", MIMEMSGPACK) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Foo, "bar") + + obj = FooStruct{} + req = requestWithBody("POST", badPath, badBody) + req.Header.Add("Content-Type", MIMEMSGPACK) + err = MsgPack.Bind(req, &obj) + assert.Error(t, err) +} + func requestWithBody(method, path, body string) (req *http.Request) { req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) return diff --git a/binding/msgpack.go b/binding/msgpack.go new file mode 100644 index 00000000..69367175 --- /dev/null +++ b/binding/msgpack.go @@ -0,0 +1,28 @@ +// Copyright 2017 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "net/http" + + "github.com/ugorji/go/codec" +) + +type msgpackBinding struct{} + +func (msgpackBinding) Name() string { + return "msgpack" +} + +func (msgpackBinding) Bind(req *http.Request, obj interface{}) error { + + if err := codec.NewDecoder(req.Body, new(codec.MsgpackHandle)).Decode(&obj); err != nil { + //var decoder *codec.Decoder = codec.NewDecoder(req.Body, &codec.MsgpackHandle) + //if err := decoder.Decode(&obj); err != nil { + return err + } + return validate(obj) + +} diff --git a/render/msgpack.go b/render/msgpack.go new file mode 100644 index 00000000..e6c13e58 --- /dev/null +++ b/render/msgpack.go @@ -0,0 +1,31 @@ +// Copyright 2017 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "net/http" + + "github.com/ugorji/go/codec" +) + +type MsgPack struct { + Data interface{} +} + +var msgpackContentType = []string{"application/msgpack; charset=utf-8"} + +func (r MsgPack) WriteContentType(w http.ResponseWriter) { + writeContentType(w, msgpackContentType) +} + +func (r MsgPack) Render(w http.ResponseWriter) error { + return WriteMsgPack(w, r.Data) +} + +func WriteMsgPack(w http.ResponseWriter, obj interface{}) error { + writeContentType(w, msgpackContentType) + var h codec.Handle = new(codec.MsgpackHandle) + return codec.NewEncoder(w, h).Encode(obj) +} diff --git a/render/render.go b/render/render.go index 7e997374..46291421 100644 --- a/render/render.go +++ b/render/render.go @@ -22,6 +22,8 @@ var ( _ HTMLRender = HTMLDebug{} _ HTMLRender = HTMLProduction{} _ Render = YAML{} + _ Render = MsgPack{} + _ Render = MsgPack{} ) func writeContentType(w http.ResponseWriter, value []string) { diff --git a/render/render_test.go b/render/render_test.go index c814ff6d..c48235c3 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -5,17 +5,40 @@ package render import ( + "bytes" "encoding/xml" "html/template" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" + "github.com/ugorji/go/codec" ) // TODO unit tests // test errors +func TestRenderMsgPack(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]interface{}{ + "foo": "bar", + } + + err := (MsgPack{data}).Render(w) + + assert.NoError(t, err) + + h := new(codec.MsgpackHandle) + assert.NotNil(t, h) + buf := bytes.NewBuffer([]byte{}) + assert.NotNil(t, buf) + err = codec.NewEncoder(buf, h).Encode(data) + + assert.NoError(t, err) + assert.Equal(t, w.Body.String(), string(buf.Bytes())) + assert.Equal(t, w.Header().Get("Content-Type"), "application/msgpack; charset=utf-8") +} + func TestRenderJSON(t *testing.T) { w := httptest.NewRecorder() data := map[string]interface{}{ diff --git a/vendor/vendor.json b/vendor/vendor.json index e754a3fd..00115c50 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -47,6 +47,12 @@ "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", From a47a8ca6e778dcdc2932107127df49447733c676 Mon Sep 17 00:00:00 2001 From: Manu MA Date: Sun, 26 Feb 2017 20:03:05 +0100 Subject: [PATCH 36/36] docs(README): fixes the markdown code format --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c940a107..7046b98e 100644 --- a/README.md +++ b/README.md @@ -103,11 +103,12 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648 ``` 4. (Optional) Use latest changes (note: they may be broken and/or unstable): -    ```sh + + ```sh $ GIN_PATH=$GOPATH/src/gopkg.in/gin-gonic/gin.v1 $ git -C $GIN_PATH checkout develop - $ git -C $GIN_PATH pull origin develop -    ``` + $ git -C $GIN_PATH pull origin develop +``` ## API Examples