Skip to content

Commit

Permalink
Add struct to structpb method that ignores omit empty tags (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
vijayvuyyuru authored Oct 3, 2023
1 parent c6d1b7e commit fcfc9d8
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 33 deletions.
50 changes: 35 additions & 15 deletions protoutils/protoutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand All @@ -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
}
Expand All @@ -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)
Expand All @@ -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
}
Expand Down
43 changes: 25 additions & 18 deletions protoutils/protoutils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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":
Expand Down Expand Up @@ -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"))
})

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit fcfc9d8

Please sign in to comment.