Skip to content

Commit

Permalink
Fix #198 (#781)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
javierprovecho authored Jan 9, 2017
1 parent 75c2274 commit 963acc4
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 32 deletions.
30 changes: 23 additions & 7 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand All @@ -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.
Expand All @@ -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.
Expand Down
132 changes: 131 additions & 1 deletion context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package gin
import (
"bytes"
"errors"
"fmt"
"html/template"
"mime/multipart"
"net/http"
Expand All @@ -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{}
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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, "<html>%s %d</html>", "string", 3)

assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}

// TestContextData tests that the response can be written from `bytesting`
// with specified MIME type
func TestContextRenderData(t *testing.T) {
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
15 changes: 9 additions & 6 deletions render/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}
7 changes: 6 additions & 1 deletion render/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
25 changes: 18 additions & 7 deletions render/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,37 @@ 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, "", " ")
}

func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj)
if err != nil {
return err
}
w.Write(jsonBytes)
return nil
}

func WriteJSON(w http.ResponseWriter, obj interface{}) error {
writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj)
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)
}
2 changes: 2 additions & 0 deletions render/redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
1 change: 1 addition & 0 deletions render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import "net/http"

type Render interface {
Render(http.ResponseWriter) error
WriteContentType(w http.ResponseWriter)
}

var (
Expand Down
5 changes: 4 additions & 1 deletion render/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ func (r String) Render(w http.ResponseWriter) error {
return nil
}

func WriteString(w http.ResponseWriter, format string, data []interface{}) {
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 {
Expand Down
Loading

0 comments on commit 963acc4

Please sign in to comment.