Skip to content

Commit 4cb495b

Browse files
authored
Add new messagemagic (#5)
* feat: support messagemagic * test: fix testdata
1 parent 139774d commit 4cb495b

File tree

6 files changed

+398
-20
lines changed

6 files changed

+398
-20
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/gochore/protomagic
33
go 1.23
44

55
require (
6+
github.com/gochore/pt v1.3.0
67
github.com/stretchr/testify v1.9.0
78
google.golang.org/protobuf v1.34.2
89
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/gochore/pt v1.3.0 h1:E/cLFk84WyNDCwFvgGZezLo656KEJNMUAznFcb5NYP0=
4+
github.com/gochore/pt v1.3.0/go.mod h1:VgAadJxZUjqIPhLPrfC8BgTWQzi1tVIrQOefxBC64PQ=
35
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
46
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
57
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

messagemagic/messagemagic.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package messagemagic
2+
3+
import (
4+
"google.golang.org/protobuf/proto"
5+
)
6+
7+
// Patch patches the message with the patch.
8+
// It copies all not optional fields from the patch to the message.
9+
// For optional fields, it copies the value only if the patch has the field.
10+
// Otherwise, it keeps the value in the message unchanged.
11+
12+
func Patch[T proto.Message](msg, patch T) T {
13+
dst := proto.Clone(msg).(T)
14+
15+
dstRefl, srcRefl := dst.ProtoReflect(), patch.ProtoReflect()
16+
for i := range srcRefl.Descriptor().Fields().Len() {
17+
fd := srcRefl.Descriptor().Fields().Get(i)
18+
if fd.HasOptionalKeyword() {
19+
if srcRefl.Has(fd) {
20+
dstRefl.Set(fd, srcRefl.Get(fd))
21+
}
22+
} else {
23+
if srcRefl.Has(fd) {
24+
dstRefl.Set(fd, srcRefl.Get(fd))
25+
} else {
26+
dstRefl.Clear(fd)
27+
}
28+
}
29+
}
30+
31+
return dst
32+
}

messagemagic/messagemagic_test.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package messagemagic
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
dummyv1 "github.com/gochore/protomagic/testdata/gen/dummy/v1"
8+
9+
"github.com/gochore/pt"
10+
"github.com/stretchr/testify/assert"
11+
"google.golang.org/protobuf/proto"
12+
)
13+
14+
func assertMessageEqual(t *testing.T, expected, actual proto.Message) {
15+
if !assert.True(t, proto.Equal(expected, actual)) {
16+
expectedJson, _ := json.Marshal(expected)
17+
actualJson, _ := json.Marshal(actual)
18+
assert.JSONEq(t, string(expectedJson), string(actualJson)) // to show the diff
19+
}
20+
}
21+
22+
func TestPatch(t *testing.T) {
23+
t.Run("regular", func(t *testing.T) {
24+
msg := &dummyv1.DummyA{
25+
Name: "name",
26+
Value: 1,
27+
Values: []string{"a", "b"},
28+
TestType: dummyv1.TestEnumType_TEST_ENUM_TYPE_FOO,
29+
ConfigA: &dummyv1.DummyConfigA{Name: "name"},
30+
OName: pt.P("o_name"),
31+
OValue: pt.P[int32](2),
32+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR),
33+
OConfigA: &dummyv1.DummyConfigA{Name: "name"},
34+
}
35+
patch := &dummyv1.DummyA{
36+
Name: "name2",
37+
Value: 2,
38+
Values: []string{"c", "d"},
39+
TestType: dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR,
40+
ConfigA: &dummyv1.DummyConfigA{Name: "name2"},
41+
OName: pt.P("o_name2"),
42+
OValue: pt.P[int32](3),
43+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_FOO),
44+
OConfigA: &dummyv1.DummyConfigA{Name: "name2"},
45+
}
46+
want := &dummyv1.DummyA{
47+
Name: "name2",
48+
Value: 2,
49+
Values: []string{"c", "d"},
50+
TestType: dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR,
51+
ConfigA: &dummyv1.DummyConfigA{Name: "name2"},
52+
OName: pt.P("o_name2"),
53+
OValue: pt.P[int32](3),
54+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_FOO),
55+
OConfigA: &dummyv1.DummyConfigA{Name: "name2"},
56+
}
57+
got := Patch(msg, patch)
58+
assertMessageEqual(t, want, got)
59+
})
60+
61+
t.Run("empty patch", func(t *testing.T) {
62+
msg := &dummyv1.DummyA{
63+
Name: "name",
64+
Value: 1,
65+
Values: []string{"a", "b"},
66+
TestType: dummyv1.TestEnumType_TEST_ENUM_TYPE_FOO,
67+
ConfigA: &dummyv1.DummyConfigA{Name: "name"},
68+
OName: pt.P("o_name"),
69+
OValue: pt.P[int32](2),
70+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR),
71+
OConfigA: &dummyv1.DummyConfigA{Name: "name"},
72+
}
73+
patch := &dummyv1.DummyA{
74+
Name: "",
75+
Value: 0,
76+
Values: nil,
77+
TestType: 0,
78+
ConfigA: nil,
79+
OName: nil,
80+
OValue: nil,
81+
OTestType: nil,
82+
OConfigA: nil,
83+
}
84+
want := &dummyv1.DummyA{
85+
Name: "",
86+
Value: 0,
87+
Values: nil,
88+
TestType: 0,
89+
ConfigA: nil,
90+
OName: pt.P("o_name"),
91+
OValue: pt.P[int32](2),
92+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR),
93+
OConfigA: &dummyv1.DummyConfigA{Name: "name"},
94+
}
95+
got := Patch(msg, patch)
96+
assertMessageEqual(t, want, got)
97+
})
98+
99+
t.Run("empty msg", func(t *testing.T) {
100+
msg := &dummyv1.DummyA{
101+
Name: "",
102+
Value: 0,
103+
Values: nil,
104+
TestType: 0,
105+
ConfigA: nil,
106+
OName: nil,
107+
OValue: nil,
108+
OTestType: nil,
109+
OConfigA: nil,
110+
}
111+
patch := &dummyv1.DummyA{
112+
Name: "name2",
113+
Value: 2,
114+
Values: []string{"c", "d"},
115+
TestType: dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR,
116+
ConfigA: &dummyv1.DummyConfigA{Name: "name2"},
117+
OName: pt.P("o_name2"),
118+
OValue: pt.P[int32](3),
119+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_FOO),
120+
OConfigA: &dummyv1.DummyConfigA{Name: "name2"},
121+
}
122+
want := &dummyv1.DummyA{
123+
Name: "name2",
124+
Value: 2,
125+
Values: []string{"c", "d"},
126+
TestType: dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR,
127+
ConfigA: &dummyv1.DummyConfigA{Name: "name2"},
128+
OName: pt.P("o_name2"),
129+
OValue: pt.P[int32](3),
130+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_FOO),
131+
OConfigA: &dummyv1.DummyConfigA{Name: "name2"},
132+
}
133+
got := Patch(msg, patch)
134+
assertMessageEqual(t, want, got)
135+
})
136+
137+
t.Run("zero patch", func(t *testing.T) {
138+
msg := &dummyv1.DummyA{
139+
Name: "name",
140+
Value: 1,
141+
Values: []string{"a", "b"},
142+
TestType: dummyv1.TestEnumType_TEST_ENUM_TYPE_FOO,
143+
ConfigA: &dummyv1.DummyConfigA{Name: "name"},
144+
OName: pt.P("o_name"),
145+
OValue: pt.P[int32](2),
146+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR),
147+
OConfigA: &dummyv1.DummyConfigA{Name: "name"},
148+
}
149+
patch := &dummyv1.DummyA{
150+
Name: "name2",
151+
Value: 2,
152+
Values: []string{"c", "d"},
153+
TestType: dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR,
154+
ConfigA: &dummyv1.DummyConfigA{Name: "name2"},
155+
OName: pt.P(""),
156+
OValue: pt.P[int32](0),
157+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_UNSPECIFIED),
158+
OConfigA: &dummyv1.DummyConfigA{},
159+
}
160+
want := &dummyv1.DummyA{
161+
Name: "name2",
162+
Value: 2,
163+
Values: []string{"c", "d"},
164+
TestType: dummyv1.TestEnumType_TEST_ENUM_TYPE_BAR,
165+
ConfigA: &dummyv1.DummyConfigA{Name: "name2"},
166+
OName: pt.P(""),
167+
OValue: pt.P[int32](0),
168+
OTestType: pt.P(dummyv1.TestEnumType_TEST_ENUM_TYPE_UNSPECIFIED),
169+
OConfigA: &dummyv1.DummyConfigA{},
170+
}
171+
got := Patch(msg, patch)
172+
assertMessageEqual(t, want, got)
173+
})
174+
}

0 commit comments

Comments
 (0)