From 9c08a58eb2064527bc5f6f2e1e5f58e116224ea6 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Tue, 28 Jan 2025 21:04:52 -0800 Subject: [PATCH] Support hooks on `embed:""` fields (#493) Relates to 840220c (#90) This change adds support for hooks to be called on fields that are tagged with `embed:""`. ### Use case If a command has several subcommands, many (but not all) of which need the same external resource, this allows defining the flag-level inputs for that resource centrally, and then using `embed:""` in any command that needs that resource. For example, imagine: ```go type githubClientProvider struct { Token string `name:"github-token" env:"GITHUB_TOKEN"` URL string `name:"github-url" env:"GITHUB_URL"` } func (g *githubClientProvider) BeforeApply(kctx *kong.Context) error { return kctx.BindToProvider(func() (*github.Client, error) { return github.NewClient(...), nil }) } ``` Then, any command that needs GitHub client will add this field, any other resource providers it needs, and add parameters to its `Run` method to accept those resources: ```go type listUsersCmd struct { GitHub githubClientProvider `embed:""` S3 s3ClientProvider `embed:""` } func (l *listUsersCmd) Run(gh *github.Client, s3 *s3.Client) error { ... } ``` ### Alternatives It is possible to do the same today if the `*Provider` struct above is actually a Go embed instead of a Kong embed, *and* it is exported. ``` type GitHubClientProvider struct{ ... } type listUsersCmd struct { GithubClientProvider S3ClientProvider } ``` The difference is whether the struct defining the flags is required to be exported or not. --- callbacks.go | 14 +++++++++++++- kong_test.go | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/callbacks.go b/callbacks.go index c1fac81..e541f5a 100644 --- a/callbacks.go +++ b/callbacks.go @@ -80,7 +80,19 @@ func getMethods(value reflect.Value, name string) []reflect.Value { for i := 0; i < value.NumField(); i++ { field := value.Field(i) fieldType := t.Field(i) - if fieldType.IsExported() && fieldType.Anonymous { + if !fieldType.IsExported() { + continue + } + + // Hooks on exported embedded fields should be called. + if fieldType.Anonymous { + receivers = append(receivers, field) + continue + } + + // Hooks on exported fields that are not exported, + // but are tagged with `embed:""` should be called. + if _, ok := fieldType.Tag.Lookup("embed"); ok { receivers = append(receivers, field) } } diff --git a/kong_test.go b/kong_test.go index a7c03b2..874b90b 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2413,9 +2413,19 @@ func (e *EmbeddedCallback) AfterApply() error { return nil } +type taggedEmbeddedCallback struct { + Tagged bool +} + +func (e *taggedEmbeddedCallback) AfterApply() error { + e.Tagged = true + return nil +} + type EmbeddedRoot struct { EmbeddedCallback - Root bool + Tagged taggedEmbeddedCallback `embed:""` + Root bool } func (e *EmbeddedRoot) AfterApply() error { @@ -2432,6 +2442,9 @@ func TestEmbeddedCallbacks(t *testing.T) { EmbeddedCallback: EmbeddedCallback{ Embedded: true, }, + Tagged: taggedEmbeddedCallback{ + Tagged: true, + }, Root: true, } assert.Equal(t, expected, actual)