Skip to content

Commit

Permalink
Merge pull request #32 from jimhickinbottom/master
Browse files Browse the repository at this point in the history
added jsonmarshaler interface
  • Loading branch information
kungfusheep authored Dec 5, 2023
2 parents 9e00d89 + 124708b commit ba5ad09
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ There are a couple of subtle ways you can configure the encoders.
* It supports the same `json:"tag,options"` syntax as the stdlib, but not the same options. Currently the options you have are
- `,stringer`, which instead of the standard serialization method for a given type, nominates that its `.String()` function is invoked instead to provide the serialization value.
- `,raw`, which allows byteslice-like items (like `[]byte` and `string`) to be written to the buffer directly with no conversion, quoting or otherwise. `nil` or empty fields annotated as `raw` will output `null`.
- `,encoder` which instead of the standard serialization method for a given type, nominates that its `.JSONEncode(*jingo.Buffer)` function is invoked instead. From there you can manually write to the buffer for that particular field. The interface you need to comply with is exported as `jingo.JSONEncoder`.
- `,encoder` which instead of the standard serialization method for a given type, nominates that its `.JSONEncode(*jingo.Buffer)` function or `EncodeJSON(io.Writer)` function are invoked instead. From there you can manually write to the buffer or writer for that particular field. There are a choice of 2 interfaces you need to comply with depending on your use case, either `jingo.JSONEncoder` (which introduces a dependency on `Buffer`), or `jingo.JSONMarshaler` which allows writing directly to an `io.Writer`.
- `,escape`, which safely escapes `"`,`\`, line feed (`\n`), carriage return (`\r`) and tab (`\t`) characters to valid JSON whilst writing. To get the same functionality when using `SliceEncoder` on its own, use `jingo.EscapeString` to initialize the encoder - e.g `NewSliceEncoder([]jingo.EscapeString)` - instead of `string` directly. There is obviously a performance impact on the write speed using this option, the benchmarks show it takes twice the time of a standard string write, so whilst it is still faster than using the stdlib, to get the best performance it is recommended to only be used when needed and only then when the escaping work can't be done up-front.


Expand Down
27 changes: 19 additions & 8 deletions jingo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"reflect"
"strconv"
"testing"
Expand All @@ -30,10 +31,11 @@ type all struct {
PropPs []*string `json:"ps"`
PropNamesEscaped []string `json:"propNameEscaped,escape"`
} `json:"propStruct"`
PropEncode encode0 `json:"propEncode,encoder"`
PropEncodeP *encode0 `json:"propEncodeP,encoder"`
PropEncodenilP *encode0 `json:"propEncodenilP,encoder"`
PropEncodeS encode1 `json:"propEncodeS,encoder"`
PropEncode encode0 `json:"propEncode,encoder"`
PropEncodeP *encode0 `json:"propEncodeP,encoder"`
PropEncodenilP *encode0 `json:"propEncodenilP,encoder"`
PropEncodeS encode1 `json:"propEncodeS,encoder"`
PropJSONMarshaler jsonMarshaler `json:"propJSONMarshaler,encoder"`
}

type encode0 struct {
Expand All @@ -55,6 +57,14 @@ func (e *encode1) JSONEncode(w *Buffer) {
}
}

type jsonMarshaler struct {
val []byte
}

func (j *jsonMarshaler) EncodeJSON(w io.Writer) {
w.Write(j.val)
}

func Example() {

enc := NewStructEncoder(all{})
Expand Down Expand Up @@ -85,15 +95,16 @@ func Example() {
PropPs: []*string{&s, nil, &s},
PropNamesEscaped: []string{"one\\two\\,three\"", "\"four\\five\\,six\""},
},
PropEncode: encode0{'1'},
PropEncodeP: &encode0{'2'},
PropEncodeS: encode1{encode0{'3'}, encode0{'4'}},
PropEncode: encode0{'1'},
PropEncodeP: &encode0{'2'},
PropEncodeS: encode1{encode0{'3'}, encode0{'4'}},
PropJSONMarshaler: jsonMarshaler{[]byte("1")},
}, b)

fmt.Println(b.String())

// Output:
// {"propBool":false,"propInt":1234567878910111212,"propInt8":123,"propInt16":12349,"propInt32":1234567891,"propInt64":1234567878910111213,"propUint":12345678789101112138,"propUint8":255,"propUint16":12345,"propUint32":1234567891,"propUint64":12345678789101112139,"propFloat32":21.232426,"propFloat64":2799999999888.2827,"propString":"thirty two thirty four","propStruct":{"propName":["a name","another name","another"],"ps":["test pointer string",null,"test pointer string"],"propNameEscaped":["one\\two\\,three\"","\"four\\five\\,six\""]},"propEncode":1,"propEncodeP":2,"propEncodenilP":null,"propEncodeS":134}
// {"propBool":false,"propInt":1234567878910111212,"propInt8":123,"propInt16":12349,"propInt32":1234567891,"propInt64":1234567878910111213,"propUint":12345678789101112138,"propUint8":255,"propUint16":12345,"propUint32":1234567891,"propUint64":12345678789101112139,"propFloat32":21.232426,"propFloat64":2799999999888.2827,"propString":"thirty two thirty four","propStruct":{"propName":["a name","another name","another"],"ps":["test pointer string",null,"test pointer string"],"propNameEscaped":["one\\two\\,three\"","\"four\\five\\,six\""]},"propEncode":1,"propEncodeP":2,"propEncodenilP":null,"propEncodeS":134,"propJSONMarshaler":1}
}

func Example_testStruct2() {
Expand Down
42 changes: 40 additions & 2 deletions structencoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package jingo

import (
"fmt"
"io"
"reflect"
"strings"
"time"
Expand Down Expand Up @@ -106,6 +107,13 @@ func NewStructEncoder(t interface{}) *StructEncoder {

/// support calling .JSONEncode(*Buffer) when the 'encoder' option is passed
case opts.Contains("encoder"):
// requrie explicit opt-in for JSONMarshaler implementation
if _, ok := reflect.PtrTo(reflect.ValueOf(e.t).Field(e.i).Type()).MethodByName("EncodeJSON"); ok {
e.optInstrEncoderWriter()
break
}

// default to JSONEncoder implementation for any other encoder fields
e.optInstrEncoder()

/// support writing byteslice-like items using 'raw' option.
Expand Down Expand Up @@ -192,6 +200,28 @@ func (e *StructEncoder) optInstrEncoder() {
}
}

func (e *StructEncoder) optInstrEncoderWriter() {
t := reflect.ValueOf(e.t).Field(e.i).Type()
if e.f.Type.Kind() == reflect.Ptr {
t = t.Elem()
}

conv := func(v unsafe.Pointer, w *Buffer) {
e, ok := reflect.NewAt(t, v).Interface().(JSONMarshaler)
if !ok {
w.Write(null)
return
}
e.EncodeJSON(w)
}

if e.f.Type.Kind() == reflect.Ptr {
e.ptrval(conv)
} else {
e.val(conv)
}
}

func (e *StructEncoder) optInstrRaw() {
conv := func(v unsafe.Pointer, w *Buffer) {
s := *(*string)(v)
Expand Down Expand Up @@ -233,7 +263,8 @@ func (e *StructEncoder) optInstrEscape() {
}

// chunk writes a chunk of body data to the chunk buffer. only for writing static
// structure and not dynamic values.
//
// structure and not dynamic values.
func (e *StructEncoder) chunk(b string) {
e.cb.Write([]byte(b))
}
Expand All @@ -252,7 +283,7 @@ func (e *StructEncoder) flunk() {
e.instructions = append(e.instructions, instruction{static: bs, kind: kindStatic})
}

/// valueInst works out the conversion function we need for `k` and creates an instruction to write it to the buffer
// valueInst works out the conversion function we need for `k` and creates an instruction to write it to the buffer
func (e *StructEncoder) valueInst(k reflect.Kind, instr func(func(unsafe.Pointer, *Buffer))) {

switch k {
Expand Down Expand Up @@ -448,6 +479,13 @@ type JSONEncoder interface {
JSONEncode(*Buffer)
}

// JSONMarshaler works with the `.encoder` option. Fields can implement this to encode their own JSON string straight
// into the provided `io.Writer`. This is useful if you require the functionality of `JSONEncoder` but don't want the hard
// dependency on `Buffer`.
type JSONMarshaler interface {
EncodeJSON(io.Writer)
}

// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
//
Expand Down

0 comments on commit ba5ad09

Please sign in to comment.