From 6095ddfdde33121eadb50c9f53e9072584d66a9a Mon Sep 17 00:00:00 2001 From: Aidan Jensen Date: Fri, 12 Jan 2024 09:39:40 -0800 Subject: [PATCH 1/4] If any messages do not have a subfilter then don't introspect them. Allows for unknown anys to be mapped Signed-off-by: Aidan Jensen --- copy.go | 8 +++++++- copy_proto_test.go | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/copy.go b/copy.go index 31b9687..c4fce24 100644 --- a/copy.go +++ b/copy.go @@ -113,6 +113,12 @@ func structToStruct(filter FieldFilter, src, dst *reflect.Value, userOptions *op return errors.Errorf("dst type is %s, expected: %s ", dst.Type(), "*any.Any") } + // If subfilter is empty then copy the entire any without any unmarshalling. + if filter.IsEmpty() { + dst.Set(*src) + break + } + srcProto, err := srcAny.UnmarshalNew() if err != nil { return errors.WithStack(err) @@ -334,7 +340,7 @@ func structToMap(filter FieldFilter, src, dst reflect.Value, userOptions *option } srcType := src.Type() for i := 0; i < src.NumField(); i++ { - srcName := fieldName(userOptions.SrcTag, srcType.Field(i)) + srcName := fieldName(userOptions.SrcTag, srcType.Field(i)) if !isExported(srcType.Field(i)) { // Unexported fields can not be copied. continue diff --git a/copy_proto_test.go b/copy_proto_test.go index 720ba14..4049ed3 100644 --- a/copy_proto_test.go +++ b/copy_proto_test.go @@ -277,6 +277,27 @@ func TestStructToStruct_NonProtoFail(t *testing.T) { assert.NotNil(t, err) } +func TestStructToStruct_UnknownAny(t *testing.T) { + userSrc := &testproto.User{ + Details: []*anypb.Any{ + { + TypeUrl: "example.com/example/UnknownType", + Value: []byte("unknown"), + }, + }, + } + userDst := &testproto.User{} + + mask := fieldmask_utils.MaskFromString("Details") + + err := fieldmask_utils.StructToStruct(mask, userSrc, userDst) + assert.NoError(t, err) + assert.Equal(t, userSrc.Details, userDst.Details) + + mask = fieldmask_utils.MaskFromString("Details{Id}") + err = fieldmask_utils.StructToStruct(mask, userSrc, userDst) + assert.Equal(t, "proto: not found", err.Error()) +} func TestStructToMap_Success(t *testing.T) { userDst := make(map[string]interface{}) mask := fieldmask_utils.MaskFromString( From cacb0dc05a8e402b623841cb45d5c6fb751a9590 Mon Sep 17 00:00:00 2001 From: Aidan Jensen Date: Fri, 12 Jan 2024 10:03:49 -0800 Subject: [PATCH 2/4] Add option to unmarshall all any. Set to true to maintain backwards compatibility Signed-off-by: Aidan Jensen --- copy.go | 19 +++++++++++++++++-- copy_proto_test.go | 38 +++++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/copy.go b/copy.go index c4fce24..e048af7 100644 --- a/copy.go +++ b/copy.go @@ -114,7 +114,7 @@ func structToStruct(filter FieldFilter, src, dst *reflect.Value, userOptions *op } // If subfilter is empty then copy the entire any without any unmarshalling. - if filter.IsEmpty() { + if filter.IsEmpty() && !userOptions.UnmarshalAllAny { dst.Set(*src) break } @@ -256,6 +256,12 @@ type options struct { // It is called before copying the data from source to destination allowing custom processing. // If the visitor function returns true the visited field is skipped. MapVisitor mapVisitor + + // UnmarshalAllAny is used to indicate unmarshal all any fields. Default to true to keep backward compatibility. + // + // If an any field is encountered and this flag is not set, it will only Unmarshal it if there is a subfilter for that field. + // If set it will always Unmarshal all any fields + UnmarshalAllAny bool } // mapVisitor is called for every filtered field in structToMap. @@ -299,9 +305,18 @@ func WithMapVisitor(visitor mapVisitor) Option { } } +func WithUnmarshalAllAny(unmarshal bool) Option { + return func(o *options) { + o.UnmarshalAllAny = unmarshal + } +} + func newDefaultOptions() *options { // set default CopyListSize is func which return src.Len() - return &options{CopyListSize: func(src *reflect.Value) int { return src.Len() }} + return &options{ + CopyListSize: func(src *reflect.Value) int { return src.Len() }, + UnmarshalAllAny: true, + } } // fieldName gets the field name according to the field's tag, or gets StructField.Name default when the field's tag is empty. diff --git a/copy_proto_test.go b/copy_proto_test.go index 4049ed3..05a504e 100644 --- a/copy_proto_test.go +++ b/copy_proto_test.go @@ -278,25 +278,41 @@ func TestStructToStruct_NonProtoFail(t *testing.T) { } func TestStructToStruct_UnknownAny(t *testing.T) { - userSrc := &testproto.User{ - Details: []*anypb.Any{ - { - TypeUrl: "example.com/example/UnknownType", - Value: []byte("unknown"), + getUsers := func() (*testproto.User, *testproto.User) { + user1 := &testproto.User{ + Details: []*anypb.Any{ + { + TypeUrl: "example.com/example/UnknownType", + Value: []byte("unknown"), + }, }, - }, + } + user2 := &testproto.User{} + return user1, user2 } - userDst := &testproto.User{} + + userWithUnknown, emptyUser := getUsers() mask := fieldmask_utils.MaskFromString("Details") - err := fieldmask_utils.StructToStruct(mask, userSrc, userDst) + err := fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser, fieldmask_utils.WithUnmarshalAllAny(false)) + assert.NoError(t, err) + assert.Equal(t, userWithUnknown.Details, emptyUser.Details) + + userWithUnknown, emptyUser = getUsers() + + err = fieldmask_utils.StructToStruct(mask, emptyUser, userWithUnknown, fieldmask_utils.WithUnmarshalAllAny(false)) assert.NoError(t, err) - assert.Equal(t, userSrc.Details, userDst.Details) + assert.Equal(t, userWithUnknown.Details, emptyUser.Details) + + userWithUnknown, emptyUser = getUsers() + + err = fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser) + assert.Equal(t, "proto:\u00a0not found", err.Error()) mask = fieldmask_utils.MaskFromString("Details{Id}") - err = fieldmask_utils.StructToStruct(mask, userSrc, userDst) - assert.Equal(t, "proto: not found", err.Error()) + err = fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser, fieldmask_utils.WithUnmarshalAllAny(false)) + assert.Equal(t, "proto:\u00a0not found", err.Error()) } func TestStructToMap_Success(t *testing.T) { userDst := make(map[string]interface{}) From 9a621734ea2b2564ca7550560c50f9344eef3ffb Mon Sep 17 00:00:00 2001 From: Aidan Jensen Date: Mon, 15 Jan 2024 08:31:16 -0800 Subject: [PATCH 3/4] Break up into multiple tests Signed-off-by: Aidan Jensen --- copy_proto_test.go | 69 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/copy_proto_test.go b/copy_proto_test.go index 05a504e..6fa0f2b 100644 --- a/copy_proto_test.go +++ b/copy_proto_test.go @@ -277,41 +277,70 @@ func TestStructToStruct_NonProtoFail(t *testing.T) { assert.NotNil(t, err) } -func TestStructToStruct_UnknownAny(t *testing.T) { - getUsers := func() (*testproto.User, *testproto.User) { - user1 := &testproto.User{ - Details: []*anypb.Any{ - { - TypeUrl: "example.com/example/UnknownType", - Value: []byte("unknown"), - }, +func TestStructToStruct_UnknownAnyInSrcNoSubfieldMask(t *testing.T) { + userWithUnknown := &testproto.User{ + Details: []*anypb.Any{ + { + TypeUrl: "example.com/example/UnknownType", + Value: []byte("unknown"), }, - } - user2 := &testproto.User{} - return user1, user2 + }, } - - userWithUnknown, emptyUser := getUsers() + emptyUser := &testproto.User{} mask := fieldmask_utils.MaskFromString("Details") - err := fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser, fieldmask_utils.WithUnmarshalAllAny(false)) assert.NoError(t, err) assert.Equal(t, userWithUnknown.Details, emptyUser.Details) +} - userWithUnknown, emptyUser = getUsers() +func TestStructToStruct_UnknownAnyInDstNoSubfieldMask(t *testing.T) { + userWithUnknown := &testproto.User{ + Details: []*anypb.Any{ + { + TypeUrl: "example.com/example/UnknownType", + Value: []byte("unknown"), + }, + }, + } + emptyUser := &testproto.User{} - err = fieldmask_utils.StructToStruct(mask, emptyUser, userWithUnknown, fieldmask_utils.WithUnmarshalAllAny(false)) + mask := fieldmask_utils.MaskFromString("Details") + err := fieldmask_utils.StructToStruct(mask, emptyUser, userWithUnknown, fieldmask_utils.WithUnmarshalAllAny(false)) assert.NoError(t, err) assert.Equal(t, userWithUnknown.Details, emptyUser.Details) +} - userWithUnknown, emptyUser = getUsers() +func TestStructToStruct_UnknownAnyDefault(t *testing.T) { + userWithUnknown := &testproto.User{ + Details: []*anypb.Any{ + { + TypeUrl: "example.com/example/UnknownType", + Value: []byte("unknown"), + }, + }, + } + emptyUser := &testproto.User{} - err = fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser) + mask := fieldmask_utils.MaskFromString("Details") + err := fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser) assert.Equal(t, "proto:\u00a0not found", err.Error()) - mask = fieldmask_utils.MaskFromString("Details{Id}") - err = fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser, fieldmask_utils.WithUnmarshalAllAny(false)) +} + +func TestStructToStruct_UnknownAnySubfieldMask(t *testing.T) { + userWithUnknown := &testproto.User{ + Details: []*anypb.Any{ + { + TypeUrl: "example.com/example/UnknownType", + Value: []byte("unknown"), + }, + }, + } + emptyUser := &testproto.User{} + + mask := fieldmask_utils.MaskFromString("Details{Id}") + err := fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser, fieldmask_utils.WithUnmarshalAllAny(false)) assert.Equal(t, "proto:\u00a0not found", err.Error()) } func TestStructToMap_Success(t *testing.T) { From 6457ef3658de5ae6d78a0356aefb297298b368f4 Mon Sep 17 00:00:00 2001 From: Aidan Jensen Date: Thu, 18 Jan 2024 07:29:27 -0800 Subject: [PATCH 4/4] match partial error string --- copy_proto_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/copy_proto_test.go b/copy_proto_test.go index 6fa0f2b..ed61ad5 100644 --- a/copy_proto_test.go +++ b/copy_proto_test.go @@ -324,8 +324,7 @@ func TestStructToStruct_UnknownAnyDefault(t *testing.T) { mask := fieldmask_utils.MaskFromString("Details") err := fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser) - assert.Equal(t, "proto:\u00a0not found", err.Error()) - + assert.Contains(t, err.Error(), "not found") } func TestStructToStruct_UnknownAnySubfieldMask(t *testing.T) { @@ -341,7 +340,7 @@ func TestStructToStruct_UnknownAnySubfieldMask(t *testing.T) { mask := fieldmask_utils.MaskFromString("Details{Id}") err := fieldmask_utils.StructToStruct(mask, userWithUnknown, emptyUser, fieldmask_utils.WithUnmarshalAllAny(false)) - assert.Equal(t, "proto:\u00a0not found", err.Error()) + assert.Contains(t, err.Error(), "not found") } func TestStructToMap_Success(t *testing.T) { userDst := make(map[string]interface{})