From 947b53d4a26f24d9842fd1539e8a65fee590a8a8 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 18 May 2015 15:45:24 +0200 Subject: [PATCH] New Render API --- context.go | 77 +++++++++++++++++++++++-------------------- context_test.go | 19 ++++++++++- gin.go | 8 ++--- render/data.go | 22 +++++-------- render/file.go | 13 ++++++++ render/html.go | 71 +++++++++++++++++---------------------- render/json.go | 32 ++++++++---------- render/redirect.go | 22 ++++++------- render/render.go | 26 +++++++-------- render/render_test.go | 57 +++++++++++--------------------- render/ssevent.go | 31 ----------------- render/text.go | 29 ++++++++-------- render/xml.go | 12 +++---- 13 files changed, 190 insertions(+), 229 deletions(-) create mode 100644 render/file.go delete mode 100644 render/ssevent.go diff --git a/context.go b/context.go index fdca9e3e..036e3e5d 100644 --- a/context.go +++ b/context.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" + "github.com/manucorporat/sse" "golang.org/x/net/context" ) @@ -315,83 +316,87 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { /******** RESPONSE RENDERING ********/ /************************************/ -func (c *Context) renderingError(err error, meta ...interface{}) { - c.ErrorTyped(err, ErrorTypeInternal, meta) - c.AbortWithStatus(500) +func (c *Context) Header(key, value string) { + if len(value) == 0 { + c.Writer.Header().Del(key) + } else { + c.Writer.Header().Set(key, value) + } } -func (c *Context) Render(code int, render render.Render, obj ...interface{}) { - if err := render.Render(c.Writer, code, obj...); err != nil { - c.renderingError(err, obj) +func (c *Context) Render(code int, r render.Render) { + w := c.Writer + w.WriteHeader(code) + if err := r.Write(w); err != nil { + debugPrintError(err) + c.ErrorTyped(err, ErrorTypeInternal, nil) + c.AbortWithStatus(500) } + //c.Abort() } // Renders the HTTP template specified by its file name. // It also updates the HTTP code and sets the Content-Type as "text/html". // See http://golang.org/doc/articles/wiki/ func (c *Context) HTML(code int, name string, obj interface{}) { - c.Render(code, c.Engine.HTMLRender, name, obj) + instance := c.Engine.HTMLRender.Instance(name, obj) + c.Render(code, instance) } func (c *Context) IndentedJSON(code int, obj interface{}) { - if err := render.WriteIndentedJSON(c.Writer, code, obj); err != nil { - c.renderingError(err, obj) - } + c.Render(code, render.IndentedJSON{Data: obj}) } // Serializes the given struct as JSON into the response body in a fast and efficient way. // It also sets the Content-Type as "application/json". func (c *Context) JSON(code int, obj interface{}) { - if err := render.WriteJSON(c.Writer, code, obj); err != nil { - c.renderingError(err, obj) - } + c.Render(code, render.JSON{Data: obj}) } // Serializes the given struct as XML into the response body in a fast and efficient way. // It also sets the Content-Type as "application/xml". func (c *Context) XML(code int, obj interface{}) { - if err := render.WriteXML(c.Writer, code, obj); err != nil { - c.renderingError(err, obj) - } + c.Render(code, render.XML{Data: obj}) } // Writes the given string into the response body and sets the Content-Type to "text/plain". func (c *Context) String(code int, format string, values ...interface{}) { - render.WritePlainText(c.Writer, code, format, values) -} - -// Writes the given string into the response body and sets the Content-Type to "text/html" without template. -func (c *Context) HTMLString(code int, format string, values ...interface{}) { - render.WriteHTMLString(c.Writer, code, format, values) + c.Render(code, render.String{ + Format: format, + Data: values}, + ) } // Returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { - render.WriteRedirect(c.Writer, code, c.Request, location) + c.Render(-1, render.Redirect{ + Code: code, + Location: location, + Request: c.Request, + }) } // Writes some data into the body stream and updates the HTTP code. func (c *Context) Data(code int, contentType string, data []byte) { - render.WriteData(c.Writer, code, contentType, data) + c.Render(code, render.Data{ + ContentType: contentType, + Data: data, + }) } // Writes the specified file into the body stream func (c *Context) File(filepath string) { - http.ServeFile(c.Writer, c.Request, filepath) + c.Render(-1, render.File{ + Path: filepath, + Request: c.Request, + }) } func (c *Context) SSEvent(name string, message interface{}) { - render.WriteSSEvent(c.Writer, name, message) -} - -func (c *Context) Header(code int, headers map[string]string) { - if len(headers) > 0 { - header := c.Writer.Header() - for key, value := range headers { - header.Set(key, value) - } - } - c.Writer.WriteHeader(code) + c.Render(-1, sse.Event{ + Event: name, + Data: message, + }) } func (c *Context) Stream(step func(w io.Writer) bool) { diff --git a/context_test.go b/context_test.go index 3c8a87ff..7cd8032b 100644 --- a/context_test.go +++ b/context_test.go @@ -215,7 +215,8 @@ func TestContextRenderString(t *testing.T) { // with Content-Type set to text/html func TestContextRenderHTMLString(t *testing.T) { c, w, _ := createTestContext() - c.HTMLString(201, "%s %d", "string", 3) + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(201, "%s %d", "string", 3) assert.Equal(t, w.Code, 201) assert.Equal(t, w.Body.String(), "string 3") @@ -233,6 +234,22 @@ func TestContextRenderData(t *testing.T) { assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") } +func TestContextHeaders(t *testing.T) { + c, _, _ := createTestContext() + c.Header("Content-Type", "text/plain") + c.Header("X-Custom", "value") + + assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/plain") + assert.Equal(t, c.Writer.Header().Get("X-Custom"), "value") + + c.Header("Content-Type", "text/html") + c.Header("X-Custom", "") + + assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/html") + _, exist := c.Writer.Header()["X-Custom"] + assert.False(t, exist) +} + // TODO func TestContextRenderRedirectWithRelativePath(t *testing.T) { c, w, _ := createTestContext() diff --git a/gin.go b/gin.go index 43b61c49..da56a634 100644 --- a/gin.go +++ b/gin.go @@ -23,7 +23,7 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { RouterGroup - HTMLRender render.Render + HTMLRender render.HTMLRender pool sync.Pool allNoRoute HandlersChain allNoMethod HandlersChain @@ -93,7 +93,7 @@ func (engine *Engine) allocateContext() (context *Context) { func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { - engine.HTMLRender = &render.HTMLDebugRender{Glob: pattern} + engine.HTMLRender = render.HTMLDebug{Glob: pattern} } else { templ := template.Must(template.ParseGlob(pattern)) engine.SetHTMLTemplate(templ) @@ -102,7 +102,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLFiles(files ...string) { if IsDebugging() { - engine.HTMLRender = &render.HTMLDebugRender{Files: files} + engine.HTMLRender = render.HTMLDebug{Files: files} } else { templ := template.Must(template.ParseFiles(files...)) engine.SetHTMLTemplate(templ) @@ -110,7 +110,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) { } func (engine *Engine) SetHTMLTemplate(templ *template.Template) { - engine.HTMLRender = render.HTMLRender{Template: templ} + engine.HTMLRender = render.HTMLProduction{Template: templ} } // Adds handlers for NoRoute. It return a 404 code by default. diff --git a/render/data.go b/render/data.go index 42f14d53..d8e42f8a 100644 --- a/render/data.go +++ b/render/data.go @@ -2,19 +2,15 @@ package render import "net/http" -type dataRender struct{} +type Data struct { + ContentType string + Data []byte +} -func (_ dataRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - contentType := data[0].(string) - bytes := data[1].([]byte) - WriteData(w, code, contentType, bytes) +func (r Data) Write(w http.ResponseWriter) error { + if len(r.ContentType) > 0 { + w.Header().Set("Content-Type", r.ContentType) + } + w.Write(r.Data) return nil } - -func WriteData(w http.ResponseWriter, code int, contentType string, data []byte) { - if len(contentType) > 0 { - w.Header().Set("Content-Type", contentType) - } - w.WriteHeader(code) - w.Write(data) -} diff --git a/render/file.go b/render/file.go new file mode 100644 index 00000000..dd7c8ece --- /dev/null +++ b/render/file.go @@ -0,0 +1,13 @@ +package render + +import "net/http" + +type File struct { + Path string + Request *http.Request +} + +func (r File) Write(w http.ResponseWriter) error { + http.ServeFile(w, r.Request, r.Path) + return nil +} diff --git a/render/html.go b/render/html.go index d7a0b898..74f31a7c 100644 --- a/render/html.go +++ b/render/html.go @@ -1,66 +1,57 @@ package render import ( - "errors" - "fmt" "html/template" "net/http" ) type ( - HTMLRender struct { + HTMLRender interface { + Instance(string, interface{}) Render + } + + HTMLProduction struct { Template *template.Template } - htmlPlainRender struct{} - - HTMLDebugRender struct { + HTMLDebug struct { Files []string Glob string } + + HTML struct { + Template *template.Template + Name string + Data interface{} + } ) -func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html; charset=utf-8") - file := data[0].(string) - args := data[1] - return html.Template.ExecuteTemplate(w, file, args) -} - -func (r *HTMLDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html; charset=utf-8") - file := data[0].(string) - obj := data[1] - - if t, err := r.loadTemplate(); err == nil { - return t.ExecuteTemplate(w, file, obj) - } else { - return err +func (r HTMLProduction) Instance(name string, data interface{}) Render { + return HTML{ + Template: r.Template, + Name: name, + Data: data, } } -func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) { +func (r HTMLDebug) Instance(name string, data interface{}) Render { + return HTML{ + Template: r.loadTemplate(), + Name: name, + Data: data, + } +} +func (r HTMLDebug) loadTemplate() *template.Template { if len(r.Files) > 0 { - return template.ParseFiles(r.Files...) + return template.Must(template.ParseFiles(r.Files...)) } if len(r.Glob) > 0 { - return template.ParseGlob(r.Glob) + return template.Must(template.ParseFiles(r.Files...)) } - return nil, errors.New("the HTML debug render was created without files or glob pattern") + panic("the HTML debug render was created without files or glob pattern") } -func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - format := data[0].(string) - values := data[1].([]interface{}) - WriteHTMLString(w, code, format, values) - return nil -} - -func WriteHTMLString(w http.ResponseWriter, code int, format string, values []interface{}) { - writeHeader(w, code, "text/html; charset=utf-8") - if len(values) > 0 { - fmt.Fprintf(w, format, values...) - } else { - w.Write([]byte(format)) - } +func (r HTML) Write(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + return r.Template.ExecuteTemplate(w, r.Name, r.Data) } diff --git a/render/json.go b/render/json.go index 5cd1fe74..57185393 100644 --- a/render/json.go +++ b/render/json.go @@ -6,30 +6,26 @@ import ( ) type ( - jsonRender struct{} + JSON struct { + Data interface{} + } - indentedJSON struct{} + IndentedJSON struct { + Data interface{} + } ) -func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - return WriteJSON(w, code, data[0]) +func (r JSON) Write(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + return json.NewEncoder(w).Encode(r.Data) } -func (_ indentedJSON) Render(w http.ResponseWriter, code int, data ...interface{}) error { - return WriteIndentedJSON(w, code, data[0]) -} - -func WriteJSON(w http.ResponseWriter, code int, data interface{}) error { - writeHeader(w, code, "application/json; charset=utf-8") - return json.NewEncoder(w).Encode(data) -} - -func WriteIndentedJSON(w http.ResponseWriter, code int, data interface{}) error { - writeHeader(w, code, "application/json; charset=utf-8") - jsonData, err := json.MarshalIndent(data, "", " ") +func (r IndentedJSON) Write(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + jsonBytes, err := json.MarshalIndent(r.Data, "", " ") if err != nil { return err } - _, err = w.Write(jsonData) - return err + w.Write(jsonBytes) + return nil } diff --git a/render/redirect.go b/render/redirect.go index 6f6e60a7..583bf16b 100644 --- a/render/redirect.go +++ b/render/redirect.go @@ -5,18 +5,16 @@ import ( "net/http" ) -type redirectRender struct{} +type Redirect struct { + Code int + Request *http.Request + Location string +} -func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - req := data[0].(*http.Request) - location := data[1].(string) - WriteRedirect(w, code, req, location) +func (r Redirect) Write(w http.ResponseWriter) error { + if r.Code < 300 || r.Code > 308 { + panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code)) + } + http.Redirect(w, r.Request, r.Location, r.Code) return nil } - -func WriteRedirect(w http.ResponseWriter, code int, req *http.Request, location string) { - if code < 300 || code > 308 { - panic(fmt.Sprintf("Cannot redirect with status code %d", code)) - } - http.Redirect(w, req, location, code) -} diff --git a/render/render.go b/render/render.go index e80958b3..57a57249 100644 --- a/render/render.go +++ b/render/render.go @@ -7,22 +7,18 @@ package render import "net/http" type Render interface { - Render(http.ResponseWriter, int, ...interface{}) error + Write(http.ResponseWriter) error } var ( - JSON Render = jsonRender{} - IndentedJSON Render = indentedJSON{} - XML Render = xmlRender{} - HTMLPlain Render = htmlPlainRender{} - Plain Render = plainTextRender{} - Redirect Render = redirectRender{} - Data Render = dataRender{} - _ Render = HTMLRender{} - _ Render = &HTMLDebugRender{} + _ Render = JSON{} + _ Render = IndentedJSON{} + _ Render = XML{} + _ Render = String{} + _ Render = Redirect{} + _ Render = Data{} + _ Render = HTML{} + _ Render = File{} + _ HTMLRender = HTMLDebug{} + _ HTMLRender = HTMLProduction{} ) - -func writeHeader(w http.ResponseWriter, code int, contentType string) { - w.Header().Set("Content-Type", contentType) - w.WriteHeader(code) -} diff --git a/render/render_test.go b/render/render_test.go index 3ecca0e9..d0bff547 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -18,30 +18,27 @@ import ( func TestRenderJSON(t *testing.T) { w := httptest.NewRecorder() - w2 := httptest.NewRecorder() data := map[string]interface{}{ "foo": "bar", } - err := JSON.Render(w, 201, data) - WriteJSON(w2, 201, data) + err := (JSON{data}).Write(w) - assert.Equal(t, w, w2) assert.NoError(t, err) - assert.Equal(t, w.Code, 201) assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") } func TestRenderIndentedJSON(t *testing.T) { w := httptest.NewRecorder() - err := IndentedJSON.Render(w, 202, map[string]interface{}{ + data := map[string]interface{}{ "foo": "bar", "bar": "foo", - }) + } + + err := (IndentedJSON{data}).Write(w) assert.NoError(t, err) - assert.Equal(t, w.Code, 202) assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}") assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") } @@ -74,17 +71,13 @@ func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { func TestRenderXML(t *testing.T) { w := httptest.NewRecorder() - w2 := httptest.NewRecorder() data := xmlmap{ "foo": "bar", } - err := XML.Render(w, 200, data) - WriteXML(w2, 200, data) + err := (XML{data}).Write(w) - assert.Equal(t, w, w2) assert.NoError(t, err) - assert.Equal(t, w.Code, 200) assert.Equal(t, w.Body.String(), "bar") assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8") } @@ -95,53 +88,43 @@ func TestRenderRedirect(t *testing.T) { func TestRenderData(t *testing.T) { w := httptest.NewRecorder() - w2 := httptest.NewRecorder() data := []byte("#!PNG some raw data") - err := Data.Render(w, 400, "image/png", data) - WriteData(w2, 400, "image/png", data) + err := (Data{ + ContentType: "image/png", + Data: data, + }).Write(w) - assert.Equal(t, w, w2) assert.NoError(t, err) - assert.Equal(t, w.Code, 400) assert.Equal(t, w.Body.String(), "#!PNG some raw data") assert.Equal(t, w.Header().Get("Content-Type"), "image/png") } -func TestRenderPlain(t *testing.T) { +func TestRenderString(t *testing.T) { w := httptest.NewRecorder() - w2 := httptest.NewRecorder() - err := Plain.Render(w, 400, "hola %s %d", []interface{}{"manu", 2}) - WritePlainText(w2, 400, "hola %s %d", []interface{}{"manu", 2}) + err := (String{ + Format: "hola %s %d", + Data: []interface{}{"manu", 2}, + }).Write(w) - assert.Equal(t, w, w2) assert.NoError(t, err) - assert.Equal(t, w.Code, 400) assert.Equal(t, w.Body.String(), "hola manu 2") assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8") } -func TestRenderPlainHTML(t *testing.T) { - w := httptest.NewRecorder() - err := HTMLPlain.Render(w, 401, "hola %s %d", []interface{}{"manu", 2}) - - assert.NoError(t, err) - assert.Equal(t, w.Code, 401) - assert.Equal(t, w.Body.String(), "hola manu 2") - assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") -} - func TestRenderHTMLTemplate(t *testing.T) { w := httptest.NewRecorder() templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) - htmlRender := HTMLRender{Template: templ} - err := htmlRender.Render(w, 402, "t", map[string]interface{}{ + + htmlRender := HTMLProduction{Template: templ} + instance := htmlRender.Instance("t", map[string]interface{}{ "name": "alexandernyquist", }) + err := instance.Write(w) + assert.NoError(t, err) - assert.Equal(t, w.Code, 402) assert.Equal(t, w.Body.String(), "Hello alexandernyquist") assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") } diff --git a/render/ssevent.go b/render/ssevent.go deleted file mode 100644 index c9dad22f..00000000 --- a/render/ssevent.go +++ /dev/null @@ -1,31 +0,0 @@ -package render - -import ( - "net/http" - - "github.com/manucorporat/sse" -) - -type sseRender struct{} - -var SSEvent Render = sseRender{} - -func (_ sseRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - eventName := data[0].(string) - obj := data[1] - return WriteSSEvent(w, eventName, obj) -} - -func WriteSSEvent(w http.ResponseWriter, eventName string, data interface{}) error { - header := w.Header() - if len(header.Get("Content-Type")) == 0 { - header.Set("Content-Type", sse.ContentType) - } - if len(header.Get("Cache-Control")) == 0 { - header.Set("Cache-Control", "no-cache") - } - return sse.Encode(w, sse.Event{ - Event: eventName, - Data: data, - }) -} diff --git a/render/text.go b/render/text.go index efd52015..020d213a 100644 --- a/render/text.go +++ b/render/text.go @@ -5,21 +5,20 @@ import ( "net/http" ) -type plainTextRender struct{} +type String struct { + Format string + Data []interface{} +} -func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - format := data[0].(string) - values := data[1].([]interface{}) - WritePlainText(w, code, format, values) +func (r String) Write(w http.ResponseWriter) error { + header := w.Header() + if _, exist := header["Content-Type"]; !exist { + header.Set("Content-Type", "text/plain; charset=utf-8") + } + if len(r.Data) > 0 { + fmt.Fprintf(w, r.Format, r.Data...) + } else { + w.Write([]byte(r.Format)) + } return nil } - -func WritePlainText(w http.ResponseWriter, code int, format string, values []interface{}) { - writeHeader(w, code, "text/plain; charset=utf-8") - // we assume w.Write can not fail, is that right? - if len(values) > 0 { - fmt.Fprintf(w, format, values...) - } else { - w.Write([]byte(format)) - } -} diff --git a/render/xml.go b/render/xml.go index 8ebe302b..2002e61b 100644 --- a/render/xml.go +++ b/render/xml.go @@ -5,13 +5,11 @@ import ( "net/http" ) -type xmlRender struct{} - -func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - return WriteXML(w, code, data[0]) +type XML struct { + Data interface{} } -func WriteXML(w http.ResponseWriter, code int, data interface{}) error { - writeHeader(w, code, "application/xml; charset=utf-8") - return xml.NewEncoder(w).Encode(data) +func (r XML) Write(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + return xml.NewEncoder(w).Encode(r.Data) }