From fcfc9d8b07e0aa48df27b09051f5bdc911f3da66 Mon Sep 17 00:00:00 2001 From: Vijay Vuyyuru Date: Tue, 3 Oct 2023 18:05:17 -0400 Subject: [PATCH] Add struct to structpb method that ignores omit empty tags (#204) --- protoutils/protoutils.go | 50 ++++++++++++++++++++++++----------- protoutils/protoutils_test.go | 43 +++++++++++++++++------------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/protoutils/protoutils.go b/protoutils/protoutils.go index 8a27b48d..ecb90f3d 100644 --- a/protoutils/protoutils.go +++ b/protoutils/protoutils.go @@ -13,6 +13,10 @@ import ( // InterfaceToMap attempts to coerce an interface into a form acceptable by structpb.NewStruct. // Expects a struct or a map-like object. func InterfaceToMap(data interface{}) (map[string]interface{}, error) { + return interfaceToMapHelper(data, false) +} + +func interfaceToMapHelper(data interface{}, ignoreOmitEmpty bool) (map[string]interface{}, error) { if data == nil { return nil, errors.New("no data passed in") } @@ -24,12 +28,12 @@ func InterfaceToMap(data interface{}) (map[string]interface{}, error) { var err error switch t.Kind() { case reflect.Struct: - res, err = structToMap(data) + res, err = structToMap(data, ignoreOmitEmpty) if err != nil { return nil, err } case reflect.Map: - res, err = marshalMap(data) + res, err = marshalMap(data, ignoreOmitEmpty) if err != nil { return nil, err } @@ -59,8 +63,23 @@ func StructToStructPb(i interface{}) (*structpb.Struct, error) { return ret, nil } +// StructToStructPbIgnoreOmitEmpty converts an arbitrary Go struct to a *structpb.Struct. Only exported fields are included in the +// returned proto and any omitempty tag is ignored. +func StructToStructPbIgnoreOmitEmpty(i interface{}) (*structpb.Struct, error) { + encoded, err := interfaceToMapHelper(i, true) + if err != nil { + return nil, errors.Wrapf(err, + "unable to convert interface %v to a form acceptable to structpb.NewStruct", i) + } + ret, err := structpb.NewStruct(encoded) + if err != nil { + return nil, errors.Wrap(err, fmt.Sprintf("unable to construct structpb.Struct from map %v", encoded)) + } + return ret, nil +} + // takes a go type and tries to make it a better type for converting to grpc. -func toInterface(data interface{}) (interface{}, error) { +func toInterface(data interface{}, ignoreOmitEmpty bool) (interface{}, error) { if data == nil { return data, nil } @@ -76,17 +95,17 @@ func toInterface(data interface{}) (interface{}, error) { var err error switch t.Kind() { case reflect.Struct: - newData, err = structToMap(data) + newData, err = structToMap(data, ignoreOmitEmpty) if err != nil { return nil, err } case reflect.Map: - newData, err = marshalMap(data) + newData, err = marshalMap(data, ignoreOmitEmpty) if err != nil { return nil, err } case reflect.Slice: - newData, err = marshalSlice(data) + newData, err = marshalSlice(data, ignoreOmitEmpty) if err != nil { return nil, err } @@ -128,8 +147,9 @@ func isEmptyValue(v reflect.Value) bool { } } -// structToMap attempts to coerce a struct into a form acceptable by grpc. Ignores omitempty tags. -func structToMap(data interface{}) (map[string]interface{}, error) { +// structToMap attempts to coerce a struct into a form acceptable by grpc. +// ignoreOmitEmpty specifies whether to ignore the omitEmpty tag. +func structToMap(data interface{}, ignoreOmitEmpty bool) (map[string]interface{}, error) { t := reflect.TypeOf(data) if t.Kind() == reflect.Ptr { t = t.Elem() @@ -165,12 +185,12 @@ func structToMap(data interface{}) (map[string]interface{}, error) { field := value.Field(i).Interface() - // If "omitempty" is specified in the tag, it ignores empty values. - if strings.Contains(tag, "omitempty") && isEmptyValue(reflect.ValueOf(field)) { + // If "omitempty" is specified in the tag and ignoreOmitEmpty is false, it ignores empty values. + if !ignoreOmitEmpty && strings.Contains(tag, "omitempty") && isEmptyValue(reflect.ValueOf(field)) { continue } - data, err := toInterface(field) + data, err := toInterface(field, ignoreOmitEmpty) if err != nil { return nil, err } @@ -181,7 +201,7 @@ func structToMap(data interface{}) (map[string]interface{}, error) { } // marshalMap attempts to coerce maps of string keys into a form acceptable by grpc. -func marshalMap(data interface{}) (map[string]interface{}, error) { +func marshalMap(data interface{}, ignoreOmitEmpty bool) (map[string]interface{}, error) { s := reflect.ValueOf(data) if s.Kind() != reflect.Map { return nil, errors.Errorf("data of type %T is not a map", data) @@ -201,7 +221,7 @@ func marshalMap(data interface{}) (map[string]interface{}, error) { key = kstringer.String() } v := iter.Value().Interface() - result[key], err = toInterface(v) + result[key], err = toInterface(v, ignoreOmitEmpty) if err != nil { return nil, err } @@ -210,7 +230,7 @@ func marshalMap(data interface{}) (map[string]interface{}, error) { } // marshalSlice attempts to coerce list data into a form acceptable by grpc. -func marshalSlice(data interface{}) ([]interface{}, error) { +func marshalSlice(data interface{}, ignoreOmitEmpty bool) ([]interface{}, error) { s := reflect.ValueOf(data) if s.Kind() != reflect.Slice { return nil, errors.Errorf("data of type %T is not a slice", data) @@ -219,7 +239,7 @@ func marshalSlice(data interface{}) ([]interface{}, error) { newList := make([]interface{}, 0, s.Len()) for i := 0; i < s.Len(); i++ { value := s.Index(i).Interface() - data, err := toInterface(value) + data, err := toInterface(value, ignoreOmitEmpty) if err != nil { return nil, err } diff --git a/protoutils/protoutils_test.go b/protoutils/protoutils_test.go index 2e224ed9..5e1794eb 100644 --- a/protoutils/protoutils_test.go +++ b/protoutils/protoutils_test.go @@ -16,6 +16,8 @@ type mapTest struct { Expected map[string]interface{} } +const ignoreOmitEmpty = false + var ( myUUIDString = "c0ab974c-f32c-11ed-a05b-0242ac120003" myUUID = uuid.MustParse(myUUIDString) @@ -147,7 +149,6 @@ func TestInterfaceToMap(t *testing.T) { test.That(t, newStruct.AsMap(), test.ShouldResemble, tc.Expected) } - //nolint:dupl for _, tc := range structTests { map1, err := InterfaceToMap(tc.Data) test.That(t, err, test.ShouldBeNil) @@ -182,24 +183,24 @@ func TestInterfaceToMap(t *testing.T) { func TestMarshalMap(t *testing.T) { t.Run("not a valid map", func(t *testing.T) { - _, err := marshalMap(simpleStruct) + _, err := marshalMap(simpleStruct, ignoreOmitEmpty) test.That(t, err, test.ShouldBeError, errors.New("data of type protoutils.SimpleStruct is not a map")) - _, err = marshalMap("1") + _, err = marshalMap("1", ignoreOmitEmpty) test.That(t, err, test.ShouldBeError, errors.New("data of type string is not a map")) - _, err = marshalMap([]string{"1"}) + _, err = marshalMap([]string{"1"}, ignoreOmitEmpty) test.That(t, err, test.ShouldBeError, errors.New("data of type []string is not a map")) - _, err = marshalMap(map[int]string{1: "1"}) + _, err = marshalMap(map[int]string{1: "1"}, ignoreOmitEmpty) test.That(t, err, test.ShouldBeError, errors.New("map keys of type int are not strings and do not implement String")) - _, err = marshalMap(map[interface{}]string{"1": "1"}) + _, err = marshalMap(map[interface{}]string{"1": "1"}, ignoreOmitEmpty) test.That(t, err, test.ShouldBeError, errors.New("map keys of type interface are not strings and do not implement String")) }) for _, tc := range mapTests { - map1, err := marshalMap(tc.Data) + map1, err := marshalMap(tc.Data, ignoreOmitEmpty) test.That(t, err, test.ShouldBeNil) test.That(t, map1, test.ShouldResemble, tc.Expected) @@ -211,19 +212,18 @@ func TestMarshalMap(t *testing.T) { func TestStructToMap(t *testing.T) { t.Run("not a struct", func(t *testing.T) { - _, err := structToMap(map[string]interface{}{"exists": true}) + _, err := structToMap(map[string]interface{}{"exists": true}, ignoreOmitEmpty) test.That(t, err, test.ShouldBeError, errors.New("data of type map[string]interface {} is not a struct")) - _, err = structToMap(1) + _, err = structToMap(1, ignoreOmitEmpty) test.That(t, err, test.ShouldBeError, errors.New("data of type int is not a struct")) - _, err = structToMap([]string{"1"}) + _, err = structToMap([]string{"1"}, ignoreOmitEmpty) test.That(t, err, test.ShouldBeError, errors.New("data of type []string is not a struct")) }) - //nolint:dupl for _, tc := range structTests { - map1, err := structToMap(tc.Data) + map1, err := structToMap(tc.Data, ignoreOmitEmpty) test.That(t, err, test.ShouldBeNil) switch tc.TestName { case "struct with uint": @@ -256,7 +256,7 @@ func TestStructToMap(t *testing.T) { func TestMarshalSlice(t *testing.T) { t.Run("not a list", func(t *testing.T) { - _, err := marshalSlice(1) + _, err := marshalSlice(1, ignoreOmitEmpty) test.That(t, err, test.ShouldBeError, errors.New("data of type int is not a slice")) }) @@ -304,7 +304,7 @@ func TestMarshalSlice(t *testing.T) { }, } { t.Run(tc.TestName, func(t *testing.T) { - marshalled, err := marshalSlice(tc.Data) + marshalled, err := marshalSlice(tc.Data, ignoreOmitEmpty) test.That(t, err, test.ShouldBeNil) test.That(t, len(marshalled), test.ShouldEqual, tc.Length) test.That(t, marshalled, test.ShouldResemble, tc.Expected) @@ -332,24 +332,31 @@ func TestStructToStructPb(t *testing.T) { } } +func TestStructToStructPbOmitEmpty(t *testing.T) { + expected := map[string]interface{}{"x": 0.0, "y": 0.0} + data, err := StructToStructPbIgnoreOmitEmpty(OmitStruct{}) + test.That(t, err, test.ShouldBeNil) + test.That(t, data.AsMap(), test.ShouldResemble, expected) +} + func TestToInterfaceWeirdBugUint(t *testing.T) { a := uint(5) - x, err := toInterface(a) + x, err := toInterface(a, ignoreOmitEmpty) test.That(t, err, test.ShouldBeNil) test.That(t, x, test.ShouldEqual, a) - x, err = toInterface(&a) + x, err = toInterface(&a, ignoreOmitEmpty) test.That(t, err, test.ShouldBeNil) test.That(t, x, test.ShouldEqual, a) } func TestToInterfaceWeirdBugUint8(t *testing.T) { a := uint8(5) - x, err := toInterface(a) + x, err := toInterface(a, ignoreOmitEmpty) test.That(t, err, test.ShouldBeNil) test.That(t, x, test.ShouldEqual, a) - x, err = toInterface(&a) + x, err = toInterface(&a, ignoreOmitEmpty) test.That(t, err, test.ShouldBeNil) test.That(t, x, test.ShouldEqual, a) }