Skip to content

Commit

Permalink
Merge pull request #20 from bet365/escape
Browse files Browse the repository at this point in the history
added ,escape option which fixes #17
  • Loading branch information
kungfusheep authored Feb 4, 2021
2 parents aa99394 + 3fecd49 commit cfca0b3
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 115 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ 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 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`.
- `,escape`, which safely escapes `"` and `\` 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.

## How does it work

Expand Down
8 changes: 7 additions & 1 deletion buffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@ func (b *Buffer) Write(v []byte) (int, error) {
return len(v), nil
}

// WriteString writes a string to the buffer
func (b *Buffer) WriteString(v string) {
b.Bytes = append(b.Bytes, v...)
}

// WriteByte writes a single byte into the output buffer
func (b *Buffer) WriteByte(v byte) {
func (b *Buffer) WriteByte(v byte) error {
b.Bytes = append(b.Bytes, v)
return nil
}

// Reset allows this to be reused by emptying
Expand Down
125 changes: 112 additions & 13 deletions jingo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ type all struct {
PropFloat64 float64 `json:"propFloat64,stringer"`
PropString string `json:"propString"`
PropStruct struct {
PropNames []string `json:"propName"`
PropPs []*string `json:"ps"`
PropNames []string `json:"propName"`
PropPs []*string `json:"ps"`
PropNamesEscaped []string `json:"propNameEscaped,escape"`
} `json:"propStruct"`
PropEncode encode0 `json:"propEncode,encoder"`
PropEncodeP *encode0 `json:"propEncodeP,encoder"`
Expand All @@ -53,7 +54,7 @@ func (e *encode1) JSONEncode(w *Buffer) {
}
}

func ExampleJsonAll() {
func Example() {

enc := NewStructEncoder(all{})
b := NewBufferFromPool()
Expand All @@ -75,11 +76,13 @@ func ExampleJsonAll() {
PropFloat64: 2799999999888.28293031999999,
PropString: "thirty two thirty four",
PropStruct: struct {
PropNames []string `json:"propName"`
PropPs []*string `json:"ps"`
PropNames []string `json:"propName"`
PropPs []*string `json:"ps"`
PropNamesEscaped []string `json:"propNameEscaped,escape"`
}{
PropNames: []string{"a name", "another name", "another"},
PropPs: []*string{&s, nil, &s},
PropNames: []string{"a name", "another name", "another"},
PropPs: []*string{&s, nil, &s},
PropNamesEscaped: []string{"one\\two\\,three\"", "\"four\\five\\,six\""},
},
PropEncode: encode0{'1'},
PropEncodeP: &encode0{'2'},
Expand All @@ -89,10 +92,10 @@ func ExampleJsonAll() {
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"]},"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}
}

func ExampleRaw() {
func Example_testStruct2() {

type testStruct2 struct {
Raw []byte `json:"raw,raw"`
Expand Down Expand Up @@ -138,6 +141,35 @@ func Test_NilStruct(t *testing.T) {
}
}

type StructWithEscapes struct {
String string `json:"str,escape"`
StringArray []string `json:"str-array,escape"`
}

func Test_StructWithEscapes(t *testing.T) {
es := StructWithEscapes{
String: "one\\two\\,three\"",
StringArray: []string{"one\\two", "three\\,four"},
}

wantJSON := `{"str":"one\\two\\,three\"","str-array":["one\\two","three\\,four"]}`

var enc = NewStructEncoder(StructWithEscapes{})
buf := NewBufferFromPool()
enc.Marshal(&es, buf)
resultJSON := buf.String()

// Ensure JSON is valid.
if !json.Valid([]byte(resultJSON)) {
t.Errorf("Not valid JSON:" + resultJSON)
}

// Compare result
if resultJSON != wantJSON {
t.Errorf("Test_StructWithScapes Failed: want JSON:" + wantJSON + " got JSON:" + resultJSON)
}
}

type UnicodeObject struct {
Chinese string `json:"chinese"`
Emoji string `json:"emoji"`
Expand Down Expand Up @@ -343,6 +375,26 @@ func BenchmarkSlice(b *testing.B) {
}
}

func BenchmarkSliceEscape(b *testing.B) {

ss := []string{
"a name",
"another name",
"another",
"and one more",
"last one, promise",
}

var enc = NewSliceEncoder([]EscapeString{})

b.StartTimer()
for i := 0; i < b.N; i++ {
buf := NewBufferFromPool()
enc.Marshal(&ss, buf)
buf.ReturnToPool()
}
}

func BenchmarkSliceStdLib(b *testing.B) {
ss := []string{
"a name",
Expand Down Expand Up @@ -384,11 +436,13 @@ var fake = &all{
PropFloat64: 2799999999888.28293031999999,
PropString: "thirty two thirty four",
PropStruct: struct {
PropNames []string `json:"propName"`
PropPs []*string `json:"ps"`
PropNames []string `json:"propName"`
PropPs []*string `json:"ps"`
PropNamesEscaped []string `json:"propNameEscaped,escape"`
}{
PropNames: []string{"a name", "another name", "another"},
PropPs: []*string{&s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s},
PropNames: []string{"a name", "another name", "another"},
PropPs: []*string{&s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s, nil, &s},
PropNamesEscaped: []string{"one\\two\\,three\"", "\"four\\five\\,six\""},
},
}

Expand Down Expand Up @@ -443,6 +497,51 @@ func NewSmallPayload() *SmallPayload {
return s
}

var smallPayload = NewSmallPayload()

func BenchmarkSmallPayload(b *testing.B) {

e := NewStructEncoder(SmallPayload{})

buf := NewBufferFromPool()

b.ResetTimer()
for i := 0; i < b.N; i++ {
e.Marshal(smallPayload, buf)
buf.Reset()
}
}

func BenchmarkSmallPayloadStdLib(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(smallPayload)
}
}

var largePayload = NewLargePayload()

func BenchmarkLargePayload(b *testing.B) {

e := NewStructEncoder(LargePayload{})
buf := NewBufferFromPool()

b.ResetTimer()
for i := 0; i < b.N; i++ {
e.Marshal(largePayload, buf)
buf.Reset()
}
}

func BenchmarkLargePayloadStdLib(b *testing.B) {

b.ResetTimer()
for i := 0; i < b.N; i++ {
json.Marshal(largePayload)
}
}

//
//
//
Expand Down
22 changes: 22 additions & 0 deletions ptrconvert.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,25 @@ func ptrStringToBuf(v unsafe.Pointer, b *Buffer) {
func ptrTimeToBuf(v unsafe.Pointer, b *Buffer) {
b.Bytes = (*time.Time)(v).AppendFormat(b.Bytes, time.RFC3339Nano)
}

func ptrEscapeStringToBuf(v unsafe.Pointer, w *Buffer) {
bs := *(*string)(v)

pos := 0
for i := 0; i < len(bs); i++ {
switch bs[i] {
case '\\', '"':
if pos < i {
w.WriteString(bs[pos:i])
}
pos = i + 1

w.WriteByte('\\')
w.WriteByte(bs[i])
}
}

if pos < len(bs) {
w.WriteString(bs[pos:])
}
}
Loading

0 comments on commit cfca0b3

Please sign in to comment.