diff --git a/render.go b/render.go index f6b3a8b..9827aca 100644 --- a/render.go +++ b/render.go @@ -134,6 +134,15 @@ type HTMLOptions struct { Layout string // Funcs added to Options.Funcs. Funcs template.FuncMap + // ContentType overrides Options.HTMLContentType for this call only. Charset is not appended. + ContentType string +} + +// CallOptions is a struct for overriding rendering Options on a per-call basis. +type CallOptions struct { + // ContentType overrides the instance-level content type for this call only. + // Charset is not appended. + ContentType string } // Render is a service that provides functions for easily writing JSON, XML, @@ -443,6 +452,8 @@ func (r *Render) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions { layout := r.opt.Layout funcs := template.FuncMap{} + var contentType string + for _, tmp := range r.opt.Funcs { for k, v := range tmp { funcs[k] = v @@ -458,11 +469,14 @@ func (r *Render) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions { for k, v := range opt.Funcs { funcs[k] = v } + + contentType = opt.ContentType } return HTMLOptions{ - Layout: layout, - Funcs: funcs, + Layout: layout, + Funcs: funcs, + ContentType: contentType, } } @@ -476,10 +490,19 @@ func (r *Render) Render(w io.Writer, e Engine, data interface{}) error { return err } +// resolveContentType returns the override ContentType if one was provided, otherwise the default. +func resolveContentType(defaultCT string, opts []CallOptions) string { + if len(opts) > 0 && opts[0].ContentType != "" { + return opts[0].ContentType + } + + return defaultCT +} + // Data writes out the raw bytes as binary data. -func (r *Render) Data(w io.Writer, status int, v []byte) error { +func (r *Render) Data(w io.Writer, status int, v []byte, opts ...CallOptions) error { head := Head{ - ContentType: r.opt.BinaryContentType, + ContentType: resolveContentType(r.opt.BinaryContentType, opts), Status: status, } @@ -515,8 +538,13 @@ func (r *Render) HTML(w io.Writer, status int, name string, binding interface{}, } } + ct := r.opt.HTMLContentType + r.compiledCharset + if opt.ContentType != "" { + ct = opt.ContentType + } + head := Head{ - ContentType: r.opt.HTMLContentType + r.compiledCharset, + ContentType: ct, Status: status, } @@ -531,9 +559,9 @@ func (r *Render) HTML(w io.Writer, status int, name string, binding interface{}, } // JSON marshals the given interface object and writes the JSON response. -func (r *Render) JSON(w io.Writer, status int, v interface{}) error { +func (r *Render) JSON(w io.Writer, status int, v interface{}, opts ...CallOptions) error { head := Head{ - ContentType: r.opt.JSONContentType + r.compiledCharset, + ContentType: resolveContentType(r.opt.JSONContentType+r.compiledCharset, opts), Status: status, } @@ -550,9 +578,9 @@ func (r *Render) JSON(w io.Writer, status int, v interface{}) error { } // JSONP marshals the given interface object and writes the JSON response. -func (r *Render) JSONP(w io.Writer, status int, callback string, v interface{}) error { +func (r *Render) JSONP(w io.Writer, status int, callback string, v interface{}, opts ...CallOptions) error { head := Head{ - ContentType: r.opt.JSONPContentType + r.compiledCharset, + ContentType: resolveContentType(r.opt.JSONPContentType+r.compiledCharset, opts), Status: status, } @@ -566,9 +594,9 @@ func (r *Render) JSONP(w io.Writer, status int, callback string, v interface{}) } // Text writes out a string as plain text. -func (r *Render) Text(w io.Writer, status int, v string) error { +func (r *Render) Text(w io.Writer, status int, v string, opts ...CallOptions) error { head := Head{ - ContentType: r.opt.TextContentType + r.compiledCharset, + ContentType: resolveContentType(r.opt.TextContentType+r.compiledCharset, opts), Status: status, } @@ -580,9 +608,9 @@ func (r *Render) Text(w io.Writer, status int, v string) error { } // XML marshals the given interface object and writes the XML response. -func (r *Render) XML(w io.Writer, status int, v interface{}) error { +func (r *Render) XML(w io.Writer, status int, v interface{}, opts ...CallOptions) error { head := Head{ - ContentType: r.opt.XMLContentType + r.compiledCharset, + ContentType: resolveContentType(r.opt.XMLContentType+r.compiledCharset, opts), Status: status, } diff --git a/render_data_test.go b/render_data_test.go index 086dc4d..170b336 100644 --- a/render_data_test.go +++ b/render_data_test.go @@ -69,3 +69,24 @@ func TestDataCustomContentType(t *testing.T) { expect(t, res.Header().Get(ContentType), "image/png") expect(t, res.Body.String(), "..png data..") } + +func TestDataPerCallContentType(t *testing.T) { + render := New() + + var err error + + h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + err = render.Data(w, http.StatusOK, []byte("..jpeg data.."), CallOptions{ + ContentType: "image/jpeg", + }) + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/foo", nil) + h.ServeHTTP(res, req) + + expectNil(t, err) + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get(ContentType), "image/jpeg") + expect(t, res.Body.String(), "..jpeg data..") +} diff --git a/render_html_test.go b/render_html_test.go index 2633270..4ce17f0 100644 --- a/render_html_test.go +++ b/render_html_test.go @@ -501,3 +501,26 @@ func TestHTMLTemplateOptionError(t *testing.T) { expectNotNil(t, err) expect(t, strings.Contains(err.Error(), "map has no entry for key"), true) } + +func TestHTMLPerCallContentType(t *testing.T) { + render := New(Options{ + Directory: "testdata/basic", + }) + + var err error + + h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + err = render.HTML(w, http.StatusOK, "hello", "gophers", HTMLOptions{ + ContentType: "application/xhtml+xml", + }) + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/foo", nil) + h.ServeHTTP(res, req) + + expectNil(t, err) + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get(ContentType), "application/xhtml+xml") + expect(t, res.Body.String(), "

Hello gophers

\n") +} diff --git a/render_json_test.go b/render_json_test.go index 26edc58..b939834 100644 --- a/render_json_test.go +++ b/render_json_test.go @@ -412,3 +412,24 @@ func TestJSONEncoderStream(t *testing.T) { expect(t, res.Header().Get(ContentType), ContentJSON+"; charset=UTF-8") expect(t, res.Body.String(), TestEncoder{}.String()) } + +func TestJSONPerCallContentType(t *testing.T) { + render := New() + + var err error + + h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + err = render.JSON(w, http.StatusOK, Greeting{"hello", "world"}, CallOptions{ + ContentType: "application/vnd.api+json", + }) + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/foo", nil) + h.ServeHTTP(res, req) + + expectNil(t, err) + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get(ContentType), "application/vnd.api+json") + expect(t, res.Body.String(), "{\"one\":\"hello\",\"two\":\"world\"}") +} diff --git a/render_jsonp_test.go b/render_jsonp_test.go index 8017420..f7cd495 100644 --- a/render_jsonp_test.go +++ b/render_jsonp_test.go @@ -110,3 +110,24 @@ func TestJSONPDisabledCharset(t *testing.T) { expect(t, res.Header().Get(ContentType), ContentJSONP) expect(t, res.Body.String(), "helloCallback({\"one\":\"hello\",\"two\":\"world\"});") } + +func TestJSONPPerCallContentType(t *testing.T) { + render := New() + + var err error + + h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + err = render.JSONP(w, http.StatusOK, "helloCallback", GreetingP{"hello", "world"}, CallOptions{ + ContentType: "application/vnd.api+json", + }) + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/foo", nil) + h.ServeHTTP(res, req) + + expectNil(t, err) + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get(ContentType), "application/vnd.api+json") + expect(t, res.Body.String(), "helloCallback({\"one\":\"hello\",\"two\":\"world\"});") +} diff --git a/render_text_test.go b/render_text_test.go index 146913b..f9baac7 100644 --- a/render_text_test.go +++ b/render_text_test.go @@ -111,3 +111,24 @@ func TestTextDisabledCharset(t *testing.T) { expect(t, res.Header().Get(ContentType), ContentText) expect(t, res.Body.String(), "Hello Text!") } + +func TestTextPerCallContentType(t *testing.T) { + render := New() + + var err error + + h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + err = render.Text(w, http.StatusOK, "Hello Text!", CallOptions{ + ContentType: "text/css", + }) + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/foo", nil) + h.ServeHTTP(res, req) + + expectNil(t, err) + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get(ContentType), "text/css") + expect(t, res.Body.String(), "Hello Text!") +} diff --git a/render_xml_test.go b/render_xml_test.go index 1fe9a87..a16536b 100644 --- a/render_xml_test.go +++ b/render_xml_test.go @@ -137,3 +137,24 @@ func TestXMLDisabledCharset(t *testing.T) { expect(t, res.Header().Get(ContentType), ContentXML) expect(t, res.Body.String(), "") } + +func TestXMLPerCallContentType(t *testing.T) { + render := New() + + var err error + + h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + err = render.XML(w, http.StatusOK, GreetingXML{One: "hello", Two: "world"}, CallOptions{ + ContentType: "application/vnd.api+xml", + }) + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/foo", nil) + h.ServeHTTP(res, req) + + expectNil(t, err) + expect(t, res.Code, http.StatusOK) + expect(t, res.Header().Get(ContentType), "application/vnd.api+xml") + expect(t, res.Body.String(), "") +}