Skip to content

Commit

Permalink
feat: allow non-structs to be used as commands (#428)
Browse files Browse the repository at this point in the history
* feat: allow non-structs to be used as commands

This small MR allows using the func-to-interface trick to implement a command (see commandFunc in kong_test.go).

This is useful e.g. for commands that have no flags or arguments of their own, but instead receive all required information via bound  parameters.

* fix: check DynamicCommand is runnable when adding
  • Loading branch information
costela authored Jul 5, 2024
1 parent 605cdd6 commit e864bb0
Show file tree
Hide file tree
Showing 4 changed files with 25 additions and 2 deletions.
3 changes: 3 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ type flattenedField struct {

func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err error) {
v = reflect.Indirect(v)
if v.Kind() != reflect.Struct {
return out, nil
}
for i := 0; i < v.NumField(); i++ {
ft := v.Type().Field(i)
fv := v.Field(i)
Expand Down
2 changes: 1 addition & 1 deletion context.go
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,7 @@ func checkMissingChildren(node *Node) error {
if len(missing) == 1 {
return fmt.Errorf("expected %s", missing[0])
}
return fmt.Errorf("expected one of %s", strings.Join(missing, ", "))
return fmt.Errorf("expected one of %s", strings.Join(missing, ", "))
}

// If we're missing any positionals and they're required, return an error.
Expand Down
18 changes: 17 additions & 1 deletion kong_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1297,16 +1297,25 @@ func (d *dynamicCommand) Run() error {
return nil
}

type commandFunc func() error

func (cf commandFunc) Run() error {
return cf()
}

func TestDynamicCommands(t *testing.T) {
cli := struct {
One struct{} `cmd:"one"`
}{}
help := &strings.Builder{}
two := &dynamicCommand{}
three := &dynamicCommand{}
fourRan := false
four := commandFunc(func() error { fourRan = true; return nil })
p := mustNew(t, &cli,
kong.DynamicCommand("two", "", "", &two),
kong.DynamicCommand("three", "", "", three, "hidden"),
kong.DynamicCommand("four", "", "", &four),
kong.Writers(help, help),
kong.Exit(func(int) {}))
kctx, err := p.Parse([]string{"two", "--flag=flag"})
Expand All @@ -1317,8 +1326,15 @@ func TestDynamicCommands(t *testing.T) {
assert.NoError(t, err)
assert.True(t, two.ran)

kctx, err = p.Parse([]string{"four"})
assert.NoError(t, err)
assert.False(t, fourRan)
err = kctx.Run()
assert.NoError(t, err)
assert.True(t, fourRan)

_, err = p.Parse([]string{"--help"})
assert.EqualError(t, err, `expected one of "one", "two"`)
assert.EqualError(t, err, `expected one of "one", "two", "four"`)
assert.NotContains(t, help.String(), "three", help.String())
}

Expand Down
4 changes: 4 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ type dynamicCommand struct {
// "tags" is a list of extra tag strings to parse, in the form <key>:"<value>".
func DynamicCommand(name, help, group string, cmd interface{}, tags ...string) Option {
return OptionFunc(func(k *Kong) error {
if run := getMethod(reflect.Indirect(reflect.ValueOf(cmd)), "Run"); !run.IsValid() {
return fmt.Errorf("kong: DynamicCommand %q must be a type with a 'Run' method; got %T", name, cmd)
}

k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{
name: name,
help: help,
Expand Down

0 comments on commit e864bb0

Please sign in to comment.