From e864bb02209592cb6e8259b74d325edf4236f47c Mon Sep 17 00:00:00 2001 From: Leo Antunes Date: Fri, 5 Jul 2024 14:51:38 +0200 Subject: [PATCH] feat: allow non-structs to be used as commands (#428) * 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 --- build.go | 3 +++ context.go | 2 +- kong_test.go | 18 +++++++++++++++++- options.go | 4 ++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/build.go b/build.go index 1cfe1c3..aed1f72 100644 --- a/build.go +++ b/build.go @@ -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) diff --git a/context.go b/context.go index 4f82813..03044ec 100644 --- a/context.go +++ b/context.go @@ -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. diff --git a/kong_test.go b/kong_test.go index f90f86d..c4ed7f6 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1297,6 +1297,12 @@ 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"` @@ -1304,9 +1310,12 @@ func TestDynamicCommands(t *testing.T) { 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"}) @@ -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()) } diff --git a/options.go b/options.go index d01aeec..3ab383b 100644 --- a/options.go +++ b/options.go @@ -89,6 +89,10 @@ type dynamicCommand struct { // "tags" is a list of extra tag strings to parse, in the form :"". 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,