Skip to content

Commit 4be6ae6

Browse files
authored
hooks: Recursively search embedded fields for methods (#494)
* hooks: Recursively search embedded fields for methods Follow up to #493 and 840220c Kong currently supports hooks on embedded fields of a parsed node, but only at the first level of embedding: ``` type mainCmd struct { FooOptions } type FooOptions struct { BarOptions } func (f *FooOptions) BeforeApply() error { // this will be called } type BarOptions struct { } func (b *BarOptions) BeforeApply() error { // this will not be called } ``` This change adds support for hooks to be defined on embedded fields of embedded fields so that the above example would work as expected. Per #493, the definition of "embedded" field is adjusted to mean: - Any anonymous (Go-embedded) field that is exported - Any non-anonymous field that is tagged with `embed:""` *Testing*: Includes a test case for embedding an anonymous field in an `embed:""` and an `embed:""` field in an anonymous field. * Use recursion to build up the list of receivers The 'receivers' parameter helps avoid constant memory allocation as the backing storage for the slice is reused across recursive calls.
1 parent 4e1757c commit 4be6ae6

File tree

2 files changed

+60
-23
lines changed

2 files changed

+60
-23
lines changed

callbacks.go

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -77,35 +77,53 @@ func getMethod(value reflect.Value, name string) reflect.Value {
7777
return method
7878
}
7979

80-
// Get methods from the given value and any embedded fields.
80+
// getMethods gets all methods with the given name from the given value
81+
// and any embedded fields.
82+
//
83+
// Returns a slice of bound methods that can be called directly.
8184
func getMethods(value reflect.Value, name string) []reflect.Value {
82-
// Collect all possible receivers
83-
receivers := []reflect.Value{value}
84-
if value.Kind() == reflect.Ptr {
85-
value = value.Elem()
86-
}
87-
if value.Kind() == reflect.Struct {
88-
t := value.Type()
89-
for i := 0; i < value.NumField(); i++ {
90-
field := value.Field(i)
91-
fieldType := t.Field(i)
92-
if !fieldType.IsExported() {
93-
continue
94-
}
85+
// Traverses embedded fields of the struct
86+
// starting from the given value to collect all possible receivers
87+
// for the given method name.
88+
var traverse func(value reflect.Value, receivers []reflect.Value) []reflect.Value
89+
traverse = func(value reflect.Value, receivers []reflect.Value) []reflect.Value {
90+
// Always consider the current value for hooks.
91+
receivers = append(receivers, value)
9592

96-
// Hooks on exported embedded fields should be called.
97-
if fieldType.Anonymous {
98-
receivers = append(receivers, field)
99-
continue
100-
}
93+
if value.Kind() == reflect.Ptr {
94+
value = value.Elem()
95+
}
96+
97+
// If the current value is a struct, also consider embedded fields.
98+
// Two kinds of embedded fields are considered if they're exported:
99+
//
100+
// - standard Go embedded fields
101+
// - fields tagged with `embed:""`
102+
if value.Kind() == reflect.Struct {
103+
t := value.Type()
104+
for i := 0; i < value.NumField(); i++ {
105+
fieldValue := value.Field(i)
106+
field := t.Field(i)
101107

102-
// Hooks on exported fields that are not exported,
103-
// but are tagged with `embed:""` should be called.
104-
if _, ok := fieldType.Tag.Lookup("embed"); ok {
105-
receivers = append(receivers, field)
108+
if !field.IsExported() {
109+
continue
110+
}
111+
112+
// Consider a field embedded if it's actually embedded
113+
// or if it's tagged with `embed:""`.
114+
_, isEmbedded := field.Tag.Lookup("embed")
115+
isEmbedded = isEmbedded || field.Anonymous
116+
if isEmbedded {
117+
receivers = traverse(fieldValue, receivers)
118+
}
106119
}
107120
}
121+
122+
return receivers
108123
}
124+
125+
receivers := traverse(value, nil /* receivers */)
126+
109127
// Search all receivers for methods
110128
var methods []reflect.Value
111129
for _, receiver := range receivers {

kong_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2405,6 +2405,8 @@ func TestProviderMethods(t *testing.T) {
24052405
}
24062406

24072407
type EmbeddedCallback struct {
2408+
Nested NestedCallback `embed:""`
2409+
24082410
Embedded bool
24092411
}
24102412

@@ -2414,6 +2416,8 @@ func (e *EmbeddedCallback) AfterApply() error {
24142416
}
24152417

24162418
type taggedEmbeddedCallback struct {
2419+
NestedCallback
2420+
24172421
Tagged bool
24182422
}
24192423

@@ -2422,6 +2426,15 @@ func (e *taggedEmbeddedCallback) AfterApply() error {
24222426
return nil
24232427
}
24242428

2429+
type NestedCallback struct {
2430+
nested bool
2431+
}
2432+
2433+
func (n *NestedCallback) AfterApply() error {
2434+
n.nested = true
2435+
return nil
2436+
}
2437+
24252438
type EmbeddedRoot struct {
24262439
EmbeddedCallback
24272440
Tagged taggedEmbeddedCallback `embed:""`
@@ -2441,9 +2454,15 @@ func TestEmbeddedCallbacks(t *testing.T) {
24412454
expected := &EmbeddedRoot{
24422455
EmbeddedCallback: EmbeddedCallback{
24432456
Embedded: true,
2457+
Nested: NestedCallback{
2458+
nested: true,
2459+
},
24442460
},
24452461
Tagged: taggedEmbeddedCallback{
24462462
Tagged: true,
2463+
NestedCallback: NestedCallback{
2464+
nested: true,
2465+
},
24472466
},
24482467
Root: true,
24492468
}

0 commit comments

Comments
 (0)