diff --git a/reflection/reflection.go b/reflection/reflection.go new file mode 100644 index 0000000..5733c61 --- /dev/null +++ b/reflection/reflection.go @@ -0,0 +1,53 @@ +package reflection + +import ( + "reflect" +) + +func walk(x interface{}, fn func(input string)) { + val := getValue(x) + + walkValue := func(value reflect.Value) { + walk(value.Interface(), fn) + } + + switch val.Kind() { + case reflect.String: + fn(val.String()) + case reflect.Slice, reflect.Array: + for i := 0; i < val.Len(); i++ { + walkValue(val.Index(i)) + } + case reflect.Struct: + for i := 0; i < val.NumField(); i++ { + walkValue(val.Field(i)) + } + case reflect.Map: + for _, key := range val.MapKeys() { + walkValue(val.MapIndex(key)) + } + case reflect.Chan: + for { + if v, ok := val.Recv(); ok { + walkValue(v) + } else { + break + } + } + case reflect.Func: + valResults := val.Call(nil) + for _, result := range valResults { + walkValue(result) + } + } +} + +func getValue(x interface{}) reflect.Value { + val := reflect.ValueOf(x) + + if val.Kind() == reflect.Pointer { + val = val.Elem() + } + + return val +} diff --git a/reflection/reflection_test.go b/reflection/reflection_test.go new file mode 100644 index 0000000..23a0901 --- /dev/null +++ b/reflection/reflection_test.go @@ -0,0 +1,162 @@ +package reflection + +import ( + "reflect" + "testing" +) + +type Person struct { + Name string + Profile Profile +} + +type Profile struct { + Age int + City string +} + +func TestWalk(t *testing.T) { + cases := []struct { + Name string + Input interface{} + ExpectedCalls []string + }{ + { + "struct with one string field", + struct { + Name string + }{"Chris"}, + []string{"Chris"}, + }, + { + "struct with two string fields", + struct { + Name string + City string + }{"Chris", "London"}, + []string{"Chris", "London"}, + }, + { + "struct with non string field", + struct { + Name string + Age int + }{"Chris", 33}, + []string{"Chris"}, + }, + { + "nested fields", + Person{ + "Chris", + Profile{33, "London"}, + }, + []string{"Chris", "London"}, + }, + { + "pointers to things", + &Person{ + "Chris", + Profile{33, "London"}, + }, + []string{"Chris", "London"}, + }, + { + "slices", + []Profile{ + {33, "London"}, + {34, "Reykjavík"}, + }, + []string{"London", "Reykjavík"}, + }, + { + "arrays", + [2]Profile{ + {33, "London"}, + {34, "Reykjavík"}, + }, + []string{"London", "Reykjavík"}, + }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + var got []string + + walk(test.Input, func(input string) { + got = append(got, input) + }) + + if !reflect.DeepEqual(got, test.ExpectedCalls) { + t.Errorf("got %v want %v", got, test.ExpectedCalls) + } + }) + } + + t.Run("with maps", func(t *testing.T) { + aMap := map[string]string{ + "Cow": "Moo", + "Sheep": "Baa", + } + + var got []string + walk(aMap, func(input string) { + got = append(got, input) + }) + + assertContains(t, got, "Moo") + assertContains(t, got, "Baa") + }) + + t.Run("with channels", func(t *testing.T) { + aChannel := make(chan Profile) + + go func() { + aChannel <- Profile{33, "Berlin"} + aChannel <- Profile{34, "Katowice"} + close(aChannel) + }() + + var got []string + want := []string{"Berlin", "Katowice"} + + walk(aChannel, func(input string) { + got = append(got, input) + }) + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, want %v", got, want) + } + }) + + t.Run("with functions", func(t *testing.T) { + aFunction := func() (Profile, Profile) { + return Profile{33, "Berlin"}, Profile{34, "Katowice"} + } + + var got []string + want := []string{"Berlin", "Katowice"} + + walk(aFunction, func(input string) { + got = append(got, input) + }) + + if !reflect.DeepEqual(got, want) { + t.Errorf("got %v, want %v", got, want) + } + }) +} + +func assertContains(t testing.TB, results []string, needle string) { + t.Helper() + contains := false + + for _, x := range results { + if x == needle { + contains = true + } + } + + if !contains { + t.Errorf("expected %v to contain %q but it didn't", results, needle) + } +}