Skip to content

Commit 212d3cd

Browse files
Merge pull request #29 from hellofresh/patch/easyjson
Improve JSON Marshalling and Unmarshalling
2 parents 13e8668 + 6d9e055 commit 212d3cd

File tree

11 files changed

+258
-40
lines changed

11 files changed

+258
-40
lines changed

aggregate/changed.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,14 @@ func (a *Changed) Metadata() metadata.Metadata {
9191
}
9292

9393
// WithMetadata Returns new instance of the change with key and value added to metadata
94-
func (a *Changed) WithMetadata(key string, value interface{}) goengine.Message {
95-
newAggregateChanged := *a
96-
newAggregateChanged.metadata = metadata.WithValue(a.metadata, key, value)
94+
func (a Changed) WithMetadata(key string, value interface{}) goengine.Message {
95+
a.metadata = metadata.WithValue(a.metadata, key, value)
9796

98-
return &newAggregateChanged
97+
return &a
9998
}
10099

101-
func (a *Changed) withVersion(version uint) *Changed {
102-
newAggregateChanged := *a
103-
newAggregateChanged.version = version
100+
func (a Changed) withVersion(version uint) *Changed {
101+
a.version = version
104102

105-
return &newAggregateChanged
103+
return &a
106104
}

aggregate/changed_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/stretchr/testify/require"
10+
911
"github.com/hellofresh/goengine"
1012
"github.com/hellofresh/goengine/aggregate"
1113
"github.com/hellofresh/goengine/metadata"
@@ -127,3 +129,22 @@ func TestReconstituteChange(t *testing.T) {
127129
}
128130
})
129131
}
132+
133+
func TestChanged_WithMetadata(t *testing.T) {
134+
expectedMetadata := metadata.WithValue(metadata.New(), "test", "value")
135+
msg, err := aggregate.ReconstituteChange(
136+
aggregate.GenerateID(),
137+
goengine.GenerateUUID(),
138+
struct{}{},
139+
metadata.New(),
140+
time.Now(),
141+
1,
142+
)
143+
require.NoError(t, err)
144+
145+
msgWithTest := msg.WithMetadata("test", "value")
146+
147+
assert.Equal(t, expectedMetadata, msgWithTest.Metadata())
148+
assert.Equal(t, metadata.New(), msg.Metadata(), "Original metadata should not be changed")
149+
assert.NotEqual(t, msg, msgWithTest, "Origional changed message should not be changed")
150+
}

driver/sql/projection.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"database/sql"
66

77
"github.com/hellofresh/goengine"
8+
"github.com/mailru/easyjson/jlexer"
89
)
910

1011
type (
@@ -57,3 +58,45 @@ type (
5758
// EventStreamLoader loads a event stream based on the provided notification and state
5859
EventStreamLoader func(ctx context.Context, conn *sql.Conn, notification *ProjectionNotification, position int64) (goengine.EventStream, error)
5960
)
61+
62+
// UnmarshalJSON supports json.Unmarshaler interface
63+
func (p *ProjectionNotification) UnmarshalJSON(data []byte) error {
64+
r := jlexer.Lexer{Data: data}
65+
p.UnmarshalEasyJSON(&r)
66+
return r.Error()
67+
}
68+
69+
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
70+
func (p *ProjectionNotification) UnmarshalEasyJSON(in *jlexer.Lexer) {
71+
isTopLevel := in.IsStart()
72+
if in.IsNull() {
73+
if isTopLevel {
74+
in.Consumed()
75+
}
76+
in.Skip()
77+
return
78+
}
79+
in.Delim('{')
80+
for !in.IsDelim('}') {
81+
key := in.UnsafeString()
82+
in.WantColon()
83+
if in.IsNull() {
84+
in.Skip()
85+
in.WantComma()
86+
continue
87+
}
88+
switch key {
89+
case "no":
90+
p.No = in.Int64()
91+
case "aggregate_id":
92+
p.AggregateID = in.String()
93+
default:
94+
in.SkipRecursive()
95+
}
96+
in.WantComma()
97+
}
98+
in.Delim('}')
99+
if isTopLevel {
100+
in.Consumed()
101+
}
102+
}

extension/pq/listener.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package pq
22

33
import (
44
"context"
5-
"encoding/json"
65
"strings"
76
"time"
87

98
"github.com/hellofresh/goengine"
109
"github.com/hellofresh/goengine/driver/sql"
1110
"github.com/lib/pq"
11+
"github.com/mailru/easyjson"
1212
)
1313

1414
// Ensure Listener implements sql.Listener
@@ -138,7 +138,7 @@ func (s *Listener) unmarshalNotification(n *pq.Notification) *sql.ProjectionNoti
138138
}
139139

140140
notification := &sql.ProjectionNotification{}
141-
if err := json.Unmarshal([]byte(n.Extra), notification); err != nil {
141+
if err := easyjson.Unmarshal([]byte(n.Extra), notification); err != nil {
142142
logger.WithError(err).Error("received invalid notification data")
143143
return nil
144144
}

metadata/metadata.go

Lines changed: 104 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,31 @@ package metadata
33
import (
44
"encoding/json"
55

6+
"github.com/mailru/easyjson"
67
"github.com/mailru/easyjson/jlexer"
8+
"github.com/mailru/easyjson/jwriter"
9+
"github.com/pkg/errors"
710
)
811

9-
// Metadata is an immutable map[string]interface{} implementation
10-
type Metadata interface {
11-
// Value returns the value associated with this context for key, or nil
12-
// if no value is associated with key. Successive calls to Value with
13-
// the same key returns the same result.
14-
Value(key string) interface{}
12+
type (
13+
// Metadata is an immutable map[string]interface{} implementation
14+
Metadata interface {
15+
// Value returns the value associated with this context for key, or nil
16+
// if no value is associated with key. Successive calls to Value with
17+
// the same key returns the same result.
18+
Value(key string) interface{}
1519

16-
// AsMap return the Metadata as a map[string]interface{}
17-
AsMap() map[string]interface{}
18-
}
20+
// AsMap return the Metadata as a map[string]interface{}
21+
AsMap() map[string]interface{}
22+
}
23+
24+
// jsonMarshaler is an interface used to speed up the json marshalling process
25+
jsonMarshaler interface {
26+
// MarshalJSONKeyValue writes the Metadata Key and Value and all it's parent Key Value pairs into the writer
27+
// If last is true it MUST omit the `,` from the last Key Value pair
28+
MarshalJSONKeyValue(out *jwriter.Writer, last bool)
29+
}
30+
)
1931

2032
// New return a new Metadata instance without any information
2133
func New() Metadata {
@@ -43,8 +55,12 @@ type emptyData int
4355
var (
4456
// Ensure emptyData implements the Metadata interface
4557
_ Metadata = new(emptyData)
46-
// Ensure valueData implements the json.Marshaler interface
58+
// Ensure valueData implements the jsonMarshaler interface
59+
_ jsonMarshaler = new(emptyData)
60+
// Ensure emptyData implements the json.Marshaler interface
4761
_ json.Marshaler = new(emptyData)
62+
// Ensure emptyData implements the easyjson.Marshaler interface
63+
_ easyjson.Marshaler = new(emptyData)
4864
)
4965

5066
func (*emptyData) Value(key string) interface{} {
@@ -59,6 +75,14 @@ func (v *emptyData) MarshalJSON() ([]byte, error) {
5975
return []byte("{}"), nil
6076
}
6177

78+
func (v *emptyData) MarshalEasyJSON(w *jwriter.Writer) {
79+
w.RawByte('{')
80+
w.RawByte('}')
81+
}
82+
83+
func (v *emptyData) MarshalJSONKeyValue(*jwriter.Writer, bool) {
84+
}
85+
6286
// valueData represents a key, value pair in a metadata chain
6387
type valueData struct {
6488
Metadata
@@ -69,8 +93,12 @@ type valueData struct {
6993
var (
7094
// Ensure valueData implements the Metadata interface
7195
_ Metadata = new(valueData)
96+
// Ensure valueData implements the jsonMarshaler interface
97+
_ jsonMarshaler = new(valueData)
7298
// Ensure valueData implements the json.Marshaler interface
7399
_ json.Marshaler = new(valueData)
100+
// Ensure valueData implements the easyjson.Marshaler interface
101+
_ easyjson.Marshaler = new(valueData)
74102
)
75103

76104
func (v *valueData) Value(key string) interface{} {
@@ -95,7 +123,33 @@ func (v *valueData) AsMap() map[string]interface{} {
95123
}
96124

97125
func (v *valueData) MarshalJSON() ([]byte, error) {
98-
return json.Marshal(v.AsMap())
126+
w := jwriter.Writer{}
127+
v.MarshalEasyJSON(&w)
128+
return w.Buffer.BuildBytes(), w.Error
129+
}
130+
131+
func (v *valueData) MarshalEasyJSON(w *jwriter.Writer) {
132+
w.RawByte('{')
133+
v.MarshalJSONKeyValue(w, true)
134+
w.RawByte('}')
135+
}
136+
137+
func (v *valueData) MarshalJSONKeyValue(out *jwriter.Writer, last bool) {
138+
marshalJSONKeyValues(out, v.Metadata)
139+
140+
out.String(v.key)
141+
out.RawByte(':')
142+
if vm, ok := v.val.(easyjson.Marshaler); ok {
143+
vm.MarshalEasyJSON(out)
144+
} else if vm, ok := v.val.(json.Marshaler); ok {
145+
out.Raw(vm.MarshalJSON())
146+
} else {
147+
out.Raw(json.Marshal(v.val))
148+
}
149+
150+
if !last {
151+
out.RawByte(',')
152+
}
99153
}
100154

101155
// UnmarshalJSON unmarshals the provided json into a Metadata instance
@@ -127,3 +181,42 @@ func UnmarshalJSON(json []byte) (Metadata, error) {
127181

128182
return metadata, in.Error()
129183
}
184+
185+
func marshalJSONKeyValues(out *jwriter.Writer, parent Metadata) {
186+
if parent == nil {
187+
return
188+
}
189+
190+
if m, ok := parent.(jsonMarshaler); ok {
191+
m.MarshalJSONKeyValue(out, false)
192+
return
193+
}
194+
195+
var (
196+
parentJSON []byte
197+
err error
198+
)
199+
if vm, ok := parent.(easyjson.Marshaler); ok {
200+
w := &jwriter.Writer{}
201+
vm.MarshalEasyJSON(out)
202+
parentJSON = w.Buffer.BuildBytes()
203+
err = w.Error
204+
} else if vm, ok := parent.(json.Marshaler); ok {
205+
parentJSON, err = vm.MarshalJSON()
206+
} else {
207+
parentJSON, err = json.Marshal(parent)
208+
}
209+
210+
if err != nil {
211+
out.Raw(parentJSON, err)
212+
return
213+
}
214+
215+
plen := len(parentJSON)
216+
if parentJSON[1] != '{' || parentJSON[plen-1] != '}' {
217+
out.Raw(parentJSON, errors.Errorf("JSON unmarshal failed for Metadata of type %T", parent))
218+
return
219+
}
220+
221+
out.Raw(parentJSON[1:plen-2], nil)
222+
}

metadata/metadata_test.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ package metadata_test
44

55
import (
66
"encoding/json"
7+
"strings"
78
"testing"
89
"time"
10+
"unicode"
911

1012
"github.com/hellofresh/goengine/metadata"
13+
"github.com/mailru/easyjson"
1114
"github.com/stretchr/testify/assert"
1215
)
1316

@@ -198,33 +201,40 @@ var jsonTestCases = []struct {
198201
},
199202
}
200203

201-
func TestMetadata_MarshalJSON(t *testing.T) {
204+
func TestMarshalJSON(t *testing.T) {
202205
for _, testCase := range jsonTestCases {
203206
t.Run(testCase.title, func(t *testing.T) {
207+
expectedJSON := strings.Map(func(r rune) rune {
208+
if unicode.IsSpace(r) {
209+
return -1
210+
}
211+
return r
212+
}, testCase.json)
213+
204214
m := testCase.metadata()
205215

206216
mJSON, err := json.Marshal(m)
207217

208-
assert.JSONEq(t, testCase.json, string(mJSON))
218+
assert.Equal(t, expectedJSON, string(mJSON))
209219
assert.NoError(t, err)
210220
})
211221
}
212222
}
213223

214-
func TestJSONMetadata_UnmarshalJSON(t *testing.T) {
224+
func TestUnmarshalJSON(t *testing.T) {
215225
for _, testCase := range jsonTestCases {
216226
t.Run(testCase.title, func(t *testing.T) {
217227
m, err := metadata.UnmarshalJSON([]byte(testCase.json))
218228

219229
// Need to use AsMap otherwise we can have inconsistent tests results.
220230
if assert.NoError(t, err) {
221-
assert.Equal(t, testCase.metadata().AsMap(), m.AsMap())
231+
assert.Equal(t, testCase.metadata(), m)
222232
}
223233
})
224234
}
225235
}
226236

227-
func BenchmarkJSONMetadata_UnmarshalJSON(b *testing.B) {
237+
func BenchmarkUnmarshalJSON(b *testing.B) {
228238
payload := []byte(`{"_aggregate_id": "b9ebca7a-c1eb-40dd-94a4-fac7c5e84fb5", "_aggregate_type": "bank_account", "_aggregate_version": 1}`)
229239

230240
b.ResetTimer()
@@ -235,3 +245,18 @@ func BenchmarkJSONMetadata_UnmarshalJSON(b *testing.B) {
235245
}
236246
}
237247
}
248+
249+
func BenchmarkMarshalJSON(b *testing.B) {
250+
m := metadata.New()
251+
m = metadata.WithValue(m, "_aggregate_id", "b9ebca7a-c1eb-40dd-94a4-fac7c5e84fb5")
252+
m = metadata.WithValue(m, "_aggregate_type", "bank_account")
253+
m = metadata.WithValue(m, "_aggregate_version", 1)
254+
255+
b.ResetTimer()
256+
for i := 0; i < b.N; i++ {
257+
_, err := easyjson.Marshal(m.(easyjson.Marshaler))
258+
if err != nil {
259+
b.Fail()
260+
}
261+
}
262+
}

0 commit comments

Comments
 (0)