[#122] protogen: Always marshal empty fields

This is how it was done previously:
a0a9b765f3/rpc/message/encoding.go (L31)

The tricky part is `[]byte` which is marshaled as `null` by easyjson
helper, but as `""` by protojson.

Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
This commit is contained in:
Evgenii Stratonikov 2024-10-07 15:00:16 +03:00
parent 3e705a3cbe
commit 29c522d5d8
20 changed files with 53 additions and 15 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -26,12 +26,38 @@ func nonZero[T protoInt]() T {
func TestStableMarshalSingle(t *testing.T) { func TestStableMarshalSingle(t *testing.T) {
t.Run("empty", func(t *testing.T) { t.Run("empty", func(t *testing.T) {
t.Run("proto", func(t *testing.T) {
input := &generated.Primitives{} input := &generated.Primitives{}
require.Zero(t, input.StableSize()) require.Zero(t, input.StableSize())
r := input.MarshalProtobuf(nil) r := input.MarshalProtobuf(nil)
require.Empty(t, r) require.Empty(t, r)
}) })
t.Run("json", func(t *testing.T) {
input := &generated.Primitives{}
r, err := input.MarshalJSON()
require.NoError(t, err)
require.NotEmpty(t, r)
var actual test.Primitives
require.NoError(t, protojson.Unmarshal(r, &actual))
t.Run("protojson compatibility", func(t *testing.T) {
data, err := protojson.MarshalOptions{EmitUnpopulated: true}.Marshal(&actual)
require.NoError(t, err)
require.JSONEq(t, string(data), string(r))
})
var actualFrostfs generated.Primitives
require.NoError(t, actualFrostfs.UnmarshalJSON(r))
if len(actualFrostfs.FieldA) == 0 {
actualFrostfs.FieldA = nil
}
require.Equal(t, input, &actualFrostfs)
primitivesEqual(t, input, &actual)
})
})
marshalCases := []struct { marshalCases := []struct {
name string name string
@ -77,13 +103,16 @@ func TestStableMarshalSingle(t *testing.T) {
require.NoError(t, protojson.Unmarshal(r, &actual)) require.NoError(t, protojson.Unmarshal(r, &actual))
t.Run("protojson compatibility", func(t *testing.T) { t.Run("protojson compatibility", func(t *testing.T) {
data, err := protojson.Marshal(&actual) data, err := protojson.MarshalOptions{EmitUnpopulated: true}.Marshal(&actual)
require.NoError(t, err) require.NoError(t, err)
require.JSONEq(t, string(data), string(r)) require.JSONEq(t, string(data), string(r))
}) })
var actualFrostfs generated.Primitives var actualFrostfs generated.Primitives
require.NoError(t, actualFrostfs.UnmarshalJSON(r)) require.NoError(t, actualFrostfs.UnmarshalJSON(r))
if len(actualFrostfs.FieldA) == 0 {
actualFrostfs.FieldA = nil
}
require.Equal(t, tc.input, &actualFrostfs) require.Equal(t, tc.input, &actualFrostfs)
primitivesEqual(t, tc.input, &actual) primitivesEqual(t, tc.input, &actual)
@ -94,7 +123,10 @@ func TestStableMarshalSingle(t *testing.T) {
func primitivesEqual(t *testing.T, a *generated.Primitives, b *test.Primitives) { func primitivesEqual(t *testing.T, a *generated.Primitives, b *test.Primitives) {
// Compare each field directly, because proto-generated code has private fields. // Compare each field directly, because proto-generated code has private fields.
require.Equal(t, len(a.FieldA), len(b.FieldA))
if len(a.FieldA) != 0 {
require.Equal(t, a.FieldA, b.FieldA) require.Equal(t, a.FieldA, b.FieldA)
}
require.Equal(t, a.FieldB, b.FieldB) require.Equal(t, a.FieldB, b.FieldB)
require.Equal(t, a.FieldC, b.FieldC) require.Equal(t, a.FieldC, b.FieldC)
require.Equal(t, a.FieldD, b.FieldD) require.Equal(t, a.FieldD, b.FieldD)

Binary file not shown.

View file

@ -192,14 +192,17 @@ func emitJSONFieldWrite(g *protogen.GeneratedFile, f *protogen.Field, name strin
selector := name + "." + f.GoName selector := name + "." + f.GoName
isNotDefault := notNil // This code is responsible for ignoring default values.
if f.Desc.IsList() { // We will restore it after having parametrized JSON marshaling.
isNotDefault = notEmpty //
} else if f.Desc.Kind() != protoreflect.MessageKind { // isNotDefault := notNil
_, isNotDefault = easyprotoKindInfo(f.Desc.Kind()) // if f.Desc.IsList() {
} // isNotDefault = notEmpty
g.P("if ", isNotDefault(selector), "{") // } else if f.Desc.Kind() != protoreflect.MessageKind {
defer g.P("}") // _, isNotDefault = easyprotoKindInfo(f.Desc.Kind())
// }
// g.P("if ", isNotDefault(selector), "{")
// defer g.P("}")
g.P("if !first { out.RawByte(','); } else { first = false; }") g.P("if !first { out.RawByte(','); } else { first = false; }")
g.P("const prefix string = ", `"\"`, fieldJSONName(f), `\":"`) g.P("const prefix string = ", `"\"`, fieldJSONName(f), `\":"`)
@ -247,7 +250,10 @@ func emitJSONFieldWrite(g *protogen.GeneratedFile, f *protogen.Field, name strin
case protoreflect.StringKind: case protoreflect.StringKind:
template = "out.String(%s)" template = "out.String(%s)"
case protoreflect.BytesKind: case protoreflect.BytesKind:
template = "out.Base64Bytes(%s)" g.P("if ", selector, "!= nil {")
g.P("out.Base64Bytes(", selector, ")")
g.P("} else { out.String(\"\") }")
return
case protoreflect.MessageKind: case protoreflect.MessageKind:
template = "%s.MarshalEasyJSON(out)" template = "%s.MarshalEasyJSON(out)"
case protoreflect.GroupKind: case protoreflect.GroupKind: