Skip to content

Commit 78d4066

Browse files
boblailwsutina
andauthored
feat: Allow configuring global hooks via Kong's functional options (#511)
Lets you pass `kong.WithBeforeApply` along with a function that supports dynamic bindings to register a `BeforeApply` hook without tying it directly to a node in the schema. Co-authored-by: Sutina Wipawiwat <[email protected]>
1 parent 1edf069 commit 78d4066

File tree

5 files changed

+127
-5
lines changed

5 files changed

+127
-5
lines changed

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,16 @@ func main() {
308308

309309
## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply()
310310

311-
If a node in the CLI, or any of its embedded fields, has a `BeforeReset(...) error`, `BeforeResolve
312-
(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those
313-
methods will be called before values are reset, before validation/assignment,
314-
and after validation/assignment, respectively.
311+
If a node in the CLI, or any of its embedded fields, implements a `BeforeReset(...) error`, `BeforeResolve
312+
(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those will be called as Kong
313+
resets, resolves, validates, and assigns values to the node.
314+
315+
| Hook | Description |
316+
| --------------- | ----------------------------------------------------------------------------------------------------------- |
317+
| `BeforeReset` | Invoked before values are reset to their defaults (as defined by the grammar) or to zero values |
318+
| `BeforeResolve` | Invoked before resolvers are applied to a node |
319+
| `BeforeApply` | Invoked before the traced command line arguments are applied to the grammar |
320+
| `AfterApply` | Invoked after command line arguments are applied to the grammar **and validated**` |
315321

316322
The `--help` flag is implemented with a `BeforeReset` hook.
317323

@@ -340,6 +346,10 @@ func main() {
340346
}
341347
```
342348

349+
It's also possible to register these hooks with the functional options
350+
`kong.WithBeforeReset`, `kong.WithBeforeResolve`, `kong.WithBeforeApply`, and
351+
`kong.WithAfterApply`.
352+
343353
## The Bind() option
344354

345355
Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context`, `*Path` and parent commands are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`.

hooks.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package kong
22

3+
// BeforeReset is a documentation-only interface describing hooks that run before defaults values are applied.
4+
type BeforeReset interface {
5+
// This is not the correct signature - see README for details.
6+
BeforeReset(args ...any) error
7+
}
8+
39
// BeforeResolve is a documentation-only interface describing hooks that run before resolvers are applied.
410
type BeforeResolve interface {
511
// This is not the correct signature - see README for details.

kong.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ type Kong struct {
7171
postBuildOptions []Option
7272
embedded []embedded
7373
dynamicCommands []*dynamicCommand
74+
75+
hooks map[string][]reflect.Value
7476
}
7577

7678
// New creates a new Kong parser on grammar.
@@ -84,6 +86,7 @@ func New(grammar any, options ...Option) (*Kong, error) {
8486
registry: NewRegistry().RegisterDefaults(),
8587
vars: Vars{},
8688
bindings: bindings{},
89+
hooks: make(map[string][]reflect.Value),
8790
helpFormatter: DefaultHelpValueFormatter,
8891
ignoreFields: make([]*regexp.Regexp, 0),
8992
flagNamer: func(s string) string {
@@ -366,7 +369,7 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
366369
default:
367370
panic("unsupported Path")
368371
}
369-
for _, method := range getMethods(value, name) {
372+
for _, method := range k.getMethods(value, name) {
370373
binds := k.bindings.clone()
371374
binds.add(ctx, trace)
372375
binds.add(trace.Node().Vars().CloneWith(k.vars))
@@ -380,6 +383,16 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
380383
return k.applyHookToDefaultFlags(ctx, ctx.Path[0].Node(), name)
381384
}
382385

386+
func (k *Kong) getMethods(value reflect.Value, name string) []reflect.Value {
387+
return append(
388+
// Identify callbacks by reflecting on value
389+
getMethods(value, name),
390+
391+
// Identify callbacks that were registered with a kong.Option
392+
k.hooks[name]...,
393+
)
394+
}
395+
383396
// Call hook on any unset flags with default values.
384397
func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error {
385398
if node == nil {

kong_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,65 @@ func TestHooks(t *testing.T) {
588588
}
589589
}
590590

591+
func TestGlobalHooks(t *testing.T) {
592+
var cli struct {
593+
One struct {
594+
Two string `kong:"arg,optional"`
595+
Three string
596+
} `cmd:""`
597+
}
598+
599+
called := []string{}
600+
log := func(name string) any {
601+
return func(value *kong.Path) error {
602+
switch {
603+
case value.App != nil:
604+
called = append(called, fmt.Sprintf("%s (app)", name))
605+
606+
case value.Positional != nil:
607+
called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Positional.Name))
608+
609+
case value.Flag != nil:
610+
called = append(called, fmt.Sprintf("%s (flag) %s", name, value.Flag.Name))
611+
612+
case value.Argument != nil:
613+
called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Argument.Name))
614+
615+
case value.Command != nil:
616+
called = append(called, fmt.Sprintf("%s (cmd) %s", name, value.Command.Name))
617+
}
618+
return nil
619+
}
620+
}
621+
p := mustNew(t, &cli,
622+
kong.WithBeforeReset(log("BeforeReset")),
623+
kong.WithBeforeResolve(log("BeforeResolve")),
624+
kong.WithBeforeApply(log("BeforeApply")),
625+
kong.WithAfterApply(log("AfterApply")),
626+
)
627+
628+
_, err := p.Parse([]string{"one", "two", "--three=THREE"})
629+
assert.NoError(t, err)
630+
assert.Equal(t, []string{
631+
"BeforeReset (app)",
632+
"BeforeReset (cmd) one",
633+
"BeforeReset (arg) two",
634+
"BeforeReset (flag) three",
635+
"BeforeResolve (app)",
636+
"BeforeResolve (cmd) one",
637+
"BeforeResolve (arg) two",
638+
"BeforeResolve (flag) three",
639+
"BeforeApply (app)",
640+
"BeforeApply (cmd) one",
641+
"BeforeApply (arg) two",
642+
"BeforeApply (flag) three",
643+
"AfterApply (app)",
644+
"AfterApply (cmd) one",
645+
"AfterApply (arg) two",
646+
"AfterApply (flag) three",
647+
}, called)
648+
}
649+
591650
func TestShort(t *testing.T) {
592651
var cli struct {
593652
Bool bool `short:"b"`

options.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,40 @@ func PostBuild(fn func(*Kong) error) Option {
123123
})
124124
}
125125

126+
// WithBeforeReset registers a hook to run before fields values are reset to their defaults
127+
// (as specified in the grammar) or to zero values.
128+
func WithBeforeReset(fn any) Option {
129+
return withHook("BeforeReset", fn)
130+
}
131+
132+
// WithBeforeResolve registers a hook to run before resolvers are applied.
133+
func WithBeforeResolve(fn any) Option {
134+
return withHook("BeforeResolve", fn)
135+
}
136+
137+
// WithBeforeApply registers a hook to run before command line arguments are applied to the grammar.
138+
func WithBeforeApply(fn any) Option {
139+
return withHook("BeforeApply", fn)
140+
}
141+
142+
// WithAfterApply registers a hook to run after values are applied to the grammar and validated.
143+
func WithAfterApply(fn any) Option {
144+
return withHook("AfterApply", fn)
145+
}
146+
147+
// withHook registers a named hook.
148+
func withHook(name string, fn any) Option {
149+
value := reflect.ValueOf(fn)
150+
if value.Kind() != reflect.Func {
151+
panic(fmt.Errorf("expected function, got %s", value.Type()))
152+
}
153+
154+
return OptionFunc(func(k *Kong) error {
155+
k.hooks[name] = append(k.hooks[name], value)
156+
return nil
157+
})
158+
}
159+
126160
// Name overrides the application name.
127161
func Name(name string) Option {
128162
return PostBuild(func(k *Kong) error {

0 commit comments

Comments
 (0)