[#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,11 +26,37 @@ func nonZero[T protoInt]() T {
func TestStableMarshalSingle(t *testing.T) {
t.Run("empty", func(t *testing.T) {
input := &generated.Primitives{}
require.Zero(t, input.StableSize())
t.Run("proto", func(t *testing.T) {
input := &generated.Primitives{}
require.Zero(t, input.StableSize())
r := input.MarshalProtobuf(nil)
require.Empty(t, r)
r := input.MarshalProtobuf(nil)
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 {
@ -77,13 +103,16 @@ func TestStableMarshalSingle(t *testing.T) {
require.NoError(t, protojson.Unmarshal(r, &actual))
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.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, tc.input, &actualFrostfs)
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) {
// Compare each field directly, because proto-generated code has private fields.
require.Equal(t, a.FieldA, b.FieldA)
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.FieldB, b.FieldB)
require.Equal(t, a.FieldC, b.FieldC)
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
isNotDefault := notNil
if f.Desc.IsList() {
isNotDefault = notEmpty
} else if f.Desc.Kind() != protoreflect.MessageKind {
_, isNotDefault = easyprotoKindInfo(f.Desc.Kind())
}
g.P("if ", isNotDefault(selector), "{")
defer g.P("}")
// This code is responsible for ignoring default values.
// We will restore it after having parametrized JSON marshaling.
//
// isNotDefault := notNil
// if f.Desc.IsList() {
// isNotDefault = notEmpty
// } else if f.Desc.Kind() != protoreflect.MessageKind {
// _, isNotDefault = easyprotoKindInfo(f.Desc.Kind())
// }
// g.P("if ", isNotDefault(selector), "{")
// defer g.P("}")
g.P("if !first { out.RawByte(','); } else { first = false; }")
g.P("const prefix string = ", `"\"`, fieldJSONName(f), `\":"`)
@ -247,7 +250,10 @@ func emitJSONFieldWrite(g *protogen.GeneratedFile, f *protogen.Field, name strin
case protoreflect.StringKind:
template = "out.String(%s)"
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:
template = "%s.MarshalEasyJSON(out)"
case protoreflect.GroupKind: