diff --git a/README.md b/README.md index 1e96fb7..80a84eb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/jingo_test.go b/jingo_test.go index 7ccdeba..368061a 100644 --- a/jingo_test.go +++ b/jingo_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "io" "reflect" "strconv" "testing" @@ -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 { @@ -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{}) @@ -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() { diff --git a/structencoder.go b/structencoder.go index 052cbfa..9cebd92 100755 --- a/structencoder.go +++ b/structencoder.go @@ -9,6 +9,7 @@ package jingo import ( "fmt" + "io" "reflect" "strings" "time" @@ -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. @@ -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) @@ -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)) } @@ -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 { @@ -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. //