Skip to content

Add util function to copy fields between structs #182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions util/copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package util

import "reflect"

// CopyFields copies any fields with the same name and type from one struct to another
func CopyFields[A any, B any](a *A, b *B) {
val := reflect.Indirect(reflect.ValueOf(a))
val2 := reflect.Indirect(reflect.ValueOf(b))
for i := 0; i < val.Type().NumField(); i++ {
name := val.Type().Field(i).Name
field := val2.FieldByName(name)
if !field.IsValid() {
continue
}
if val.Type().Field(i).Type != field.Type() {
continue
}
if !val.Type().Field(i).IsExported() {
continue
}
field.Set(reflect.Indirect(reflect.ValueOf(a)).FieldByName(name))
}
}
Comment on lines +6 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea to reduce boilerplate! My main concern with reflection here is that it sacrifices compile-time type safety and could lead to subtle runtime bugs, especially if API struct definitions change. Instead, I’d suggest keeping explicit field copies (or even using simple codegen if we end up needing multiple converters). Adding a quick test using reflection to catch drift between struct fields could be great though and would also help us avoid silent breakages. If we think that this is a common issue and want to go with the codegen approach, I just did a quick search and found some libraries that seem to simplify this, and I'd be happy to prototype it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah same Q can this be solved by embedded structs? As someone who's done quite a few similar things (usually compile time via macros, but confined in a specific domain like SQL objs) this feels like a slippery slope to something akin to lombok, i.e. signs that we're either doing it wrong or using the wrong lang when we need to extend it 😂

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW we can get this free-ish if the structs are protobuf and matching fields have the same type+id. The same []byte can be decoded by any schema that has the same type+id, although some human still need to verify that the field name & semantics match.

160 changes: 160 additions & 0 deletions util/copy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package util

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCopyFields_Normal(t *testing.T) {
type a struct {
Thing int
AnotherThing string
OneMoreThing bool
}

type b struct {
Thing int
AnotherThing string
}

thing1 := &a{
Thing: 123,
AnotherThing: "abc",
OneMoreThing: false,
}

thing2 := &b{}

CopyFields(thing1, thing2)
assert.Equal(t, thing2.Thing, 123)
assert.Equal(t, thing2.AnotherThing, "abc")
}

func TestCopyFields_Unexported(t *testing.T) {
type a struct {
thing int
anotherThing string
oneMoreThing bool
}

type b struct {
thing int
anotherThing string
}

thing1 := &a{
thing: 123,
anotherThing: "abc",
oneMoreThing: false,
}

thing2 := &b{}

CopyFields(thing1, thing2)
assert.Equal(t, thing2.thing, 0)
assert.Equal(t, thing2.anotherThing, "")
}

func TestCopyFields_DifferentType(t *testing.T) {
type a struct {
Thing int
AnotherThing string
OneMoreThing bool
}

type b struct {
Thing int
AnotherThing string
OneMoreThing string
}

thing1 := &a{
Thing: 123,
AnotherThing: "abc",
OneMoreThing: false,
}

thing2 := &b{}

CopyFields(thing1, thing2)
assert.Equal(t, thing2.Thing, 123)
assert.Equal(t, thing2.AnotherThing, "abc")
assert.Equal(t, thing2.OneMoreThing, "")
}

func TestCopyFields_UnexportedReceiver(t *testing.T) {
type a struct {
Thing int
AnotherThing string
OneMoreThing bool
}

type b struct {
thing int
anotherThing string
}

thing1 := &a{
Thing: 123,
AnotherThing: "abc",
OneMoreThing: false,
}

thing2 := &b{}

CopyFields(thing1, thing2)
assert.Equal(t, thing2.thing, 0)
assert.Equal(t, thing2.anotherThing, "")
}

func TestCopyFields_RefField(t *testing.T) {
type a struct {
Thing map[string]string
AnotherThing string
}

type b struct {
Thing map[string]string
}

thing1 := &a{
Thing: map[string]string{
"a": "b",
"c": "d",
},
}

thing2 := &b{}

CopyFields(thing1, thing2)
assert.Equal(t, thing2.Thing, map[string]string{
"a": "b",
"c": "d",
})
}

func TestCopyFields_Func(t *testing.T) {
type a struct {
Thing func(int) string
AnotherThing int
}

type b struct {
Thing func(int) string
AnotherThing string
}

thing1 := &a{
Thing: func(i int) string {
return fmt.Sprintf("hello %d", i)
},
}

thing2 := &b{}

CopyFields(thing1, thing2)
assert.Equal(t, thing2.Thing(123), "hello 123")
assert.Equal(t, thing2.AnotherThing, "")
}