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(), "