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()
|
## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply()
|
||||||
|
|
||||||
If a node in the CLI, or any of its embedded fields, has a `BeforeReset(...) error`, `BeforeResolve
|
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
|
(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those will be called as Kong
|
||||||
methods will be called before values are reset, before validation/assignment,
|
resets, resolves, validates, and assigns values to the node.
|
||||||
and after validation/assignment, respectively.
|
|
||||||
|
| 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.
|
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
|
## 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()`.
|
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
|
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.
|
// BeforeResolve is a documentation-only interface describing hooks that run before resolvers are applied.
|
||||||
type BeforeResolve interface {
|
type BeforeResolve interface {
|
||||||
// This is not the correct signature - see README for details.
|
// This is not the correct signature - see README for details.
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ type Kong struct {
|
|||||||
postBuildOptions []Option
|
postBuildOptions []Option
|
||||||
embedded []embedded
|
embedded []embedded
|
||||||
dynamicCommands []*dynamicCommand
|
dynamicCommands []*dynamicCommand
|
||||||
|
|
||||||
|
hooks map[string][]reflect.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Kong parser on grammar.
|
// New creates a new Kong parser on grammar.
|
||||||
@@ -84,6 +86,7 @@ func New(grammar any, options ...Option) (*Kong, error) {
|
|||||||
registry: NewRegistry().RegisterDefaults(),
|
registry: NewRegistry().RegisterDefaults(),
|
||||||
vars: Vars{},
|
vars: Vars{},
|
||||||
bindings: bindings{},
|
bindings: bindings{},
|
||||||
|
hooks: make(map[string][]reflect.Value),
|
||||||
helpFormatter: DefaultHelpValueFormatter,
|
helpFormatter: DefaultHelpValueFormatter,
|
||||||
ignoreFields: make([]*regexp.Regexp, 0),
|
ignoreFields: make([]*regexp.Regexp, 0),
|
||||||
flagNamer: func(s string) string {
|
flagNamer: func(s string) string {
|
||||||
@@ -366,7 +369,7 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
|
|||||||
default:
|
default:
|
||||||
panic("unsupported Path")
|
panic("unsupported Path")
|
||||||
}
|
}
|
||||||
for _, method := range getMethods(value, name) {
|
for _, method := range k.getMethods(value, name) {
|
||||||
binds := k.bindings.clone()
|
binds := k.bindings.clone()
|
||||||
binds.add(ctx, trace)
|
binds.add(ctx, trace)
|
||||||
binds.add(trace.Node().Vars().CloneWith(k.vars))
|
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)
|
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.
|
// Call hook on any unset flags with default values.
|
||||||
func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error {
|
func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error {
|
||||||
if node == nil {
|
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) {
|
func TestShort(t *testing.T) {
|
||||||
var cli struct {
|
var cli struct {
|
||||||
Bool bool `short:"b"`
|
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.
|
// Name overrides the application name.
|
||||||
func Name(name string) Option {
|
func Name(name string) Option {
|
||||||
return PostBuild(func(k *Kong) error {
|
return PostBuild(func(k *Kong) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user