소스 검색

New Render API

Manu Mtz-Almeida 10 년 전
부모
커밋
947b53d4a2
13개의 변경된 파일185개의 추가작업 그리고 224개의 파일을 삭제
  1. 41 36
      context.go
  2. 18 1
      context_test.go
  3. 4 4
      gin.go
  4. 8 12
      render/data.go
  5. 13 0
      render/file.go
  6. 30 39
      render/html.go
  7. 14 18
      render/json.go
  8. 9 11
      render/redirect.go
  9. 11 15
      render/render.go
  10. 20 37
      render/render_test.go
  11. 0 31
      render/ssevent.go
  12. 12 13
      render/text.go
  13. 5 7
      render/xml.go

+ 41 - 36
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) {

+ 18 - 1
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, "<html>%s %d</html>", "string", 3)
+	c.Header("Content-Type", "text/html; charset=utf-8")
+	c.String(201, "<html>%s %d</html>", "string", 3)
 
 	assert.Equal(t, w.Code, 201)
 	assert.Equal(t, w.Body.String(), "<html>string 3</html>")
@@ -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()

+ 4 - 4
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.

+ 8 - 12
render/data.go

@@ -2,19 +2,15 @@ package render
 
 import "net/http"
 
-type dataRender struct{}
-
-func (_ dataRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	contentType := data[0].(string)
-	bytes := data[1].([]byte)
-	WriteData(w, code, contentType, bytes)
-	return nil
+type Data struct {
+	ContentType string
+	Data        []byte
 }
 
-func WriteData(w http.ResponseWriter, code int, contentType string, data []byte) {
-	if len(contentType) > 0 {
-		w.Header().Set("Content-Type", contentType)
+func (r Data) Write(w http.ResponseWriter) error {
+	if len(r.ContentType) > 0 {
+		w.Header().Set("Content-Type", r.ContentType)
 	}
-	w.WriteHeader(code)
-	w.Write(data)
+	w.Write(r.Data)
+	return nil
 }

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

+ 30 - 39
render/html.go

@@ -1,66 +1,57 @@
 package render
 
 import (
-	"errors"
-	"fmt"
 	"html/template"
 	"net/http"
 )
 
 type (
-	HTMLRender struct {
-		Template *template.Template
+	HTMLRender interface {
+		Instance(string, interface{}) Render
 	}
 
-	htmlPlainRender struct{}
+	HTMLProduction struct {
+		Template *template.Template
+	}
 
-	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 HTMLProduction) Instance(name string, data interface{}) Render {
+	return HTML{
+		Template: r.Template,
+		Name:     name,
+		Data:     data,
+	}
 }
 
-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 HTMLDebug) Instance(name string, data interface{}) Render {
+	return HTML{
+		Template: r.loadTemplate(),
+		Name:     name,
+		Data:     data,
 	}
 }
-
-func (r *HTMLDebugRender) loadTemplate() (*template.Template, error) {
+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)
 }

+ 14 - 18
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 (_ 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 (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 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
 }

+ 9 - 11
render/redirect.go

@@ -5,18 +5,16 @@ import (
 	"net/http"
 )
 
-type redirectRender struct{}
-
-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)
-	return nil
+type Redirect struct {
+	Code     int
+	Request  *http.Request
+	Location string
 }
 
-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))
+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, req, location, code)
+	http.Redirect(w, r.Request, r.Location, r.Code)
+	return nil
 }

+ 11 - 15
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)
-}

+ 20 - 37
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(), "<map><foo>bar</foo></map>")
 	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")
 }

+ 0 - 31
render/ssevent.go

@@ -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,
-	})
-}

+ 12 - 13
render/text.go

@@ -5,21 +5,20 @@ import (
 	"net/http"
 )
 
-type plainTextRender struct{}
-
-func (_ plainTextRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
-	format := data[0].(string)
-	values := data[1].([]interface{})
-	WritePlainText(w, code, format, values)
-	return nil
+type String struct {
+	Format string
+	Data   []interface{}
 }
 
-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...)
+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(format))
+		w.Write([]byte(r.Format))
 	}
+	return nil
 }

+ 5 - 7
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)
 }