protogen: Always marshal empty fields
Some checks failed
DCO action / DCO (pull_request) Failing after 56s
Tests and linters / Tests with -race (pull_request) Successful in 1m10s
Tests and linters / Lint (pull_request) Successful in 1m15s
Tests and linters / Tests (pull_request) Successful in 1m29s

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 1ec1762d3a
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) {
t.Run("empty", func(t *testing.T) {
t.Run("proto", func(t *testing.T) {
input := &generated.Primitives{}
require.Zero(t, input.StableSize())
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 {
name string
@ -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, 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: