From 024a5290cac2efc50b643d5382555c5880a24bb2 Mon Sep 17 00:00:00 2001 From: SlIdE42 <17254419+SlIdE42@users.noreply.github.com> Date: Sun, 29 Sep 2024 01:47:09 +0200 Subject: [PATCH] Allow custom JSON encoder (like jsoniter) (#105) --- README.md | 4 +++ engine.go | 12 +++++++-- render.go | 10 +++++++ render_json_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 043ad12..58ec566 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,9 @@ r := render.New(render.Options{ HTMLTemplateOption: "missingkey=error", // Sets the option value for HTML templates. See https://pkg.go.dev/html/template#Template.Option for a list of known options. RequirePartials: true, // Return an error if a template is missing a partial used in a layout. DisableHTTPErrorRendering: true, // Disables automatic rendering of http.StatusInternalServerError when an error occurs. + JSONEncoder: func(w io.Writer) render.JSONEncoder { // Use jsoniter "github.com/json-iterator" + return jsoniter.NewEncoder(w) + }, }) // ... ~~~ @@ -146,6 +149,7 @@ r := render.New(render.Options{ DisableHTTPErrorRendering: false, RenderPartialsWithoutPrefix: false, BufferPool: GenericBufferPool, + JSONEncoder: nil, }) ~~~ diff --git a/engine.go b/engine.go index 6880428..afeff96 100644 --- a/engine.go +++ b/engine.go @@ -34,6 +34,13 @@ type HTML struct { bp GenericBufferPool } +// JSONEncoder match encoding/json.Encoder capabilities. +type JSONEncoder interface { + Encode(v interface{}) error + SetEscapeHTML(on bool) + SetIndent(prefix, indent string) +} + // JSON built-in renderer. type JSON struct { Head @@ -41,6 +48,7 @@ type JSON struct { UnEscapeHTML bool Prefix []byte StreamingJSON bool + NewEncoder func(w io.Writer) JSONEncoder } // JSONP built-in renderer. @@ -114,7 +122,7 @@ func (j JSON) Render(w io.Writer, v interface{}) error { } var buf bytes.Buffer - encoder := json.NewEncoder(&buf) + encoder := j.NewEncoder(&buf) encoder.SetEscapeHTML(!j.UnEscapeHTML) if j.Indent { @@ -155,7 +163,7 @@ func (j JSON) renderStreamingJSON(w io.Writer, v interface{}) error { _, _ = w.Write(j.Prefix) } - encoder := json.NewEncoder(w) + encoder := j.NewEncoder(w) encoder.SetEscapeHTML(!j.UnEscapeHTML) if j.Indent { diff --git a/render.go b/render.go index 02d949b..9f48b8c 100644 --- a/render.go +++ b/render.go @@ -2,6 +2,7 @@ package render import ( "bytes" + "encoding/json" "fmt" "html/template" "io" @@ -117,6 +118,8 @@ type Options struct { // BufferPool to use when rendering HTML templates. If none is supplied // defaults to SizedBufferPool of size 32 with 512KiB buffers. BufferPool GenericBufferPool + // Custom JSON Encoder. Default to encoding/json.NewEncoder. + JSONEncoder func(w io.Writer) JSONEncoder } // HTMLOptions is a struct for overriding some rendering Options for specific HTML call. @@ -209,6 +212,12 @@ func (r *Render) prepareOptions() { } else { r.lock = &emptyLock{} } + + if r.opt.JSONEncoder == nil { + r.opt.JSONEncoder = func(w io.Writer) JSONEncoder { + return json.NewEncoder(w) + } + } } func (r *Render) CompileTemplates() { @@ -526,6 +535,7 @@ func (r *Render) JSON(w io.Writer, status int, v interface{}) error { Prefix: r.opt.PrefixJSON, UnEscapeHTML: r.opt.UnEscapeHTML, StreamingJSON: r.opt.StreamingJSON, + NewEncoder: r.opt.JSONEncoder, } return r.Render(w, j, v) diff --git a/render_json_test.go b/render_json_test.go index 7ef5f9b..35b74de 100644 --- a/render_json_test.go +++ b/render_json_test.go @@ -2,6 +2,7 @@ package render import ( "encoding/json" + "io" "math" "net/http" "net/http/httptest" @@ -13,6 +14,28 @@ type Greeting struct { Two string `json:"two"` } +type TestEncoder struct { + JSONEncoder + w io.Writer +} + +func (e TestEncoder) NewEncoder(w io.Writer) JSONEncoder { + return TestEncoder{w: w} +} + +func (e TestEncoder) Encode(v interface{}) error { + e.w.Write([]byte(e.String())) + return nil +} + +func (e TestEncoder) SetEscapeHTML(on bool) {} + +func (e TestEncoder) SetIndent(prefix, indent string) {} + +func (e TestEncoder) String() string { + return "{\"one\":\"world\",\"two\":\"hello\"}" +} + func TestJSONBasic(t *testing.T) { render := New() @@ -346,3 +369,46 @@ func TestJSONDisabledCharset(t *testing.T) { expect(t, res.Header().Get(ContentType), ContentJSON) expect(t, res.Body.String(), "{\"one\":\"hello\",\"two\":\"world\"}") } + +func TestJSONEncoder(t *testing.T) { + render := New(Options{ + JSONEncoder: TestEncoder{}.NewEncoder, + }) + + var err error + + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err = render.JSON(w, 299, Greeting{"hello", "world"}) + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/foo", nil) + h.ServeHTTP(res, req) + + expectNil(t, err) + expect(t, res.Code, 299) + expect(t, res.Header().Get(ContentType), ContentJSON+"; charset=UTF-8") + expect(t, res.Body.String(), TestEncoder{}.String()) +} + +func TestJSONEncoderStream(t *testing.T) { + render := New(Options{ + JSONEncoder: TestEncoder{}.NewEncoder, + StreamingJSON: true, + }) + + var err error + + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err = render.JSON(w, 299, Greeting{"hello", "world"}) + }) + + res := httptest.NewRecorder() + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/foo", nil) + h.ServeHTTP(res, req) + + expectNil(t, err) + expect(t, res.Code, 299) + expect(t, res.Header().Get(ContentType), ContentJSON+"; charset=UTF-8") + expect(t, res.Body.String(), TestEncoder{}.String()) +}