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 <swipawiwat@squareup.com>
This commit is contained in:
@@ -308,10 +308,16 @@ func main() {
|
||||
|
||||
## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply()
|
||||
|
||||
If a node in the CLI, or any of its embedded fields, has a `BeforeReset(...) error`, `BeforeResolve
|
||||
(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those
|
||||
methods will be called before values are reset, before validation/assignment,
|
||||
and after validation/assignment, respectively.
|
||||
If a node in the CLI, or any of its embedded fields, implements a `BeforeReset(...) error`, `BeforeResolve
|
||||
(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those will be called as Kong
|
||||
resets, resolves, validates, and assigns values to the node.
|
||||
|
||||
| Hook | Description |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `BeforeReset` | Invoked before values are reset to their defaults (as defined by the grammar) or to zero values |
|
||||
| `BeforeResolve` | Invoked before resolvers are applied to a node |
|
||||
| `BeforeApply` | Invoked before the traced command line arguments are applied to the grammar |
|
||||
| `AfterApply` | Invoked after command line arguments are applied to the grammar **and validated**` |
|
||||
|
||||
The `--help` flag is implemented with a `BeforeReset` hook.
|
||||
|
||||
@@ -340,6 +346,10 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
It's also possible to register these hooks with the functional options
|
||||
`kong.WithBeforeReset`, `kong.WithBeforeResolve`, `kong.WithBeforeApply`, and
|
||||
`kong.WithAfterApply`.
|
||||
|
||||
## The Bind() option
|
||||
|
||||
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()`.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package kong
|
||||
|
||||
// BeforeReset is a documentation-only interface describing hooks that run before defaults values are applied.
|
||||
type BeforeReset interface {
|
||||
// This is not the correct signature - see README for details.
|
||||
BeforeReset(args ...any) error
|
||||
}
|
||||
|
||||
// BeforeResolve is a documentation-only interface describing hooks that run before resolvers are applied.
|
||||
type BeforeResolve interface {
|
||||
// This is not the correct signature - see README for details.
|
||||
|
||||
@@ -71,6 +71,8 @@ type Kong struct {
|
||||
postBuildOptions []Option
|
||||
embedded []embedded
|
||||
dynamicCommands []*dynamicCommand
|
||||
|
||||
hooks map[string][]reflect.Value
|
||||
}
|
||||
|
||||
// New creates a new Kong parser on grammar.
|
||||
@@ -84,6 +86,7 @@ func New(grammar any, options ...Option) (*Kong, error) {
|
||||
registry: NewRegistry().RegisterDefaults(),
|
||||
vars: Vars{},
|
||||
bindings: bindings{},
|
||||
hooks: make(map[string][]reflect.Value),
|
||||
helpFormatter: DefaultHelpValueFormatter,
|
||||
ignoreFields: make([]*regexp.Regexp, 0),
|
||||
flagNamer: func(s string) string {
|
||||
@@ -366,7 +369,7 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
|
||||
default:
|
||||
panic("unsupported Path")
|
||||
}
|
||||
for _, method := range getMethods(value, name) {
|
||||
for _, method := range k.getMethods(value, name) {
|
||||
binds := k.bindings.clone()
|
||||
binds.add(ctx, trace)
|
||||
binds.add(trace.Node().Vars().CloneWith(k.vars))
|
||||
@@ -380,6 +383,16 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
|
||||
return k.applyHookToDefaultFlags(ctx, ctx.Path[0].Node(), name)
|
||||
}
|
||||
|
||||
func (k *Kong) getMethods(value reflect.Value, name string) []reflect.Value {
|
||||
return append(
|
||||
// Identify callbacks by reflecting on value
|
||||
getMethods(value, name),
|
||||
|
||||
// Identify callbacks that were registered with a kong.Option
|
||||
k.hooks[name]...,
|
||||
)
|
||||
}
|
||||
|
||||
// Call hook on any unset flags with default values.
|
||||
func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error {
|
||||
if node == nil {
|
||||
|
||||
@@ -588,6 +588,65 @@ func TestHooks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalHooks(t *testing.T) {
|
||||
var cli struct {
|
||||
One struct {
|
||||
Two string `kong:"arg,optional"`
|
||||
Three string
|
||||
} `cmd:""`
|
||||
}
|
||||
|
||||
called := []string{}
|
||||
log := func(name string) any {
|
||||
return func(value *kong.Path) error {
|
||||
switch {
|
||||
case value.App != nil:
|
||||
called = append(called, fmt.Sprintf("%s (app)", name))
|
||||
|
||||
case value.Positional != nil:
|
||||
called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Positional.Name))
|
||||
|
||||
case value.Flag != nil:
|
||||
called = append(called, fmt.Sprintf("%s (flag) %s", name, value.Flag.Name))
|
||||
|
||||
case value.Argument != nil:
|
||||
called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Argument.Name))
|
||||
|
||||
case value.Command != nil:
|
||||
called = append(called, fmt.Sprintf("%s (cmd) %s", name, value.Command.Name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
p := mustNew(t, &cli,
|
||||
kong.WithBeforeReset(log("BeforeReset")),
|
||||
kong.WithBeforeResolve(log("BeforeResolve")),
|
||||
kong.WithBeforeApply(log("BeforeApply")),
|
||||
kong.WithAfterApply(log("AfterApply")),
|
||||
)
|
||||
|
||||
_, err := p.Parse([]string{"one", "two", "--three=THREE"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{
|
||||
"BeforeReset (app)",
|
||||
"BeforeReset (cmd) one",
|
||||
"BeforeReset (arg) two",
|
||||
"BeforeReset (flag) three",
|
||||
"BeforeResolve (app)",
|
||||
"BeforeResolve (cmd) one",
|
||||
"BeforeResolve (arg) two",
|
||||
"BeforeResolve (flag) three",
|
||||
"BeforeApply (app)",
|
||||
"BeforeApply (cmd) one",
|
||||
"BeforeApply (arg) two",
|
||||
"BeforeApply (flag) three",
|
||||
"AfterApply (app)",
|
||||
"AfterApply (cmd) one",
|
||||
"AfterApply (arg) two",
|
||||
"AfterApply (flag) three",
|
||||
}, called)
|
||||
}
|
||||
|
||||
func TestShort(t *testing.T) {
|
||||
var cli struct {
|
||||
Bool bool `short:"b"`
|
||||
|
||||
+34
@@ -123,6 +123,40 @@ func PostBuild(fn func(*Kong) error) Option {
|
||||
})
|
||||
}
|
||||
|
||||
// WithBeforeReset registers a hook to run before fields values are reset to their defaults
|
||||
// (as specified in the grammar) or to zero values.
|
||||
func WithBeforeReset(fn any) Option {
|
||||
return withHook("BeforeReset", fn)
|
||||
}
|
||||
|
||||
// WithBeforeResolve registers a hook to run before resolvers are applied.
|
||||
func WithBeforeResolve(fn any) Option {
|
||||
return withHook("BeforeResolve", fn)
|
||||
}
|
||||
|
||||
// WithBeforeApply registers a hook to run before command line arguments are applied to the grammar.
|
||||
func WithBeforeApply(fn any) Option {
|
||||
return withHook("BeforeApply", fn)
|
||||
}
|
||||
|
||||
// WithAfterApply registers a hook to run after values are applied to the grammar and validated.
|
||||
func WithAfterApply(fn any) Option {
|
||||
return withHook("AfterApply", fn)
|
||||
}
|
||||
|
||||
// withHook registers a named hook.
|
||||
func withHook(name string, fn any) Option {
|
||||
value := reflect.ValueOf(fn)
|
||||
if value.Kind() != reflect.Func {
|
||||
panic(fmt.Errorf("expected function, got %s", value.Type()))
|
||||
}
|
||||
|
||||
return OptionFunc(func(k *Kong) error {
|
||||
k.hooks[name] = append(k.hooks[name], value)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Name overrides the application name.
|
||||
func Name(name string) Option {
|
||||
return PostBuild(func(k *Kong) error {
|
||||
|
||||
Reference in New Issue
Block a user