diff --git a/.circleci/config.yml b/.circleci/config.yml index 076df0f..763b2f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: command: | go get -v github.com/jstemmer/go-junit-report go get -v -t -d ./... - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s v1.23.1 + curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s v1.23.7 mkdir ~/report when: always - run: diff --git a/.golangci.yml b/.golangci.yml index a5b2806..af55257 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,8 +19,6 @@ linters: linters-settings: govet: check-shadowing: true - gocyclo: - min-complexity: 10 dupl: threshold: 100 goconst: @@ -41,3 +39,4 @@ issues: - 'composite literal uses unkeyed fields' - 'bad syntax for struct tag key' - 'bad syntax for struct tag pair' + - 'result .* \(error\) is always nil' diff --git a/callbacks.go b/callbacks.go index e403d27..139a83b 100644 --- a/callbacks.go +++ b/callbacks.go @@ -6,7 +6,7 @@ import ( "strings" ) -type bindings map[reflect.Type]reflect.Value +type bindings map[reflect.Type]func() (reflect.Value, error) func (b bindings) String() string { out := []string{} @@ -18,7 +18,8 @@ func (b bindings) String() string { func (b bindings) add(values ...interface{}) bindings { for _, v := range values { - b[reflect.TypeOf(v)] = reflect.ValueOf(v) + v := v + b[reflect.TypeOf(v)] = func() (reflect.Value, error) { return reflect.ValueOf(v), nil } } return b } @@ -57,8 +58,12 @@ func callMethod(name string, v, f reflect.Value, bindings bindings) error { } for i := 0; i < t.NumIn(); i++ { pt := t.In(i) - if arg, ok := bindings[pt]; ok { - in = append(in, arg) + if argf, ok := bindings[pt]; ok { + argv, err := argf() + if err != nil { + return err + } + in = append(in, argv) } else { return fmt.Errorf("couldn't find binding of type %s for parameter %d of %s.%s(), use kong.Bind(%s)", pt, i, v.Type(), name, pt) } diff --git a/context.go b/context.go index 334b855..9e1231a 100644 --- a/context.go +++ b/context.go @@ -91,7 +91,8 @@ func (c *Context) Bind(args ...interface{}) { // // BindTo(impl, (*MyInterface)(nil)) func (c *Context) BindTo(impl, iface interface{}) { - c.bindings[reflect.TypeOf(iface).Elem()] = reflect.ValueOf(impl) + valueOf := reflect.ValueOf(impl) + c.bindings[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil } } // Value returns the value for a particular path element. diff --git a/options.go b/options.go index 1bf5219..66fd633 100644 --- a/options.go +++ b/options.go @@ -6,6 +6,8 @@ import ( "path/filepath" "reflect" "strings" + + "github.com/pkg/errors" ) // An Option applies optional changes to the Kong application. @@ -145,7 +147,33 @@ func Bind(args ...interface{}) Option { // BindTo(impl, (*iface)(nil)) func BindTo(impl, iface interface{}) Option { return OptionFunc(func(k *Kong) error { - k.bindings[reflect.TypeOf(iface).Elem()] = reflect.ValueOf(impl) + valueOf := reflect.ValueOf(impl) + k.bindings[reflect.TypeOf(iface).Elem()] = func() (reflect.Value, error) { return valueOf, nil } + return nil + }) +} + +// BindToProvider allows binding of provider functions. +// +// This is useful when the Run() function of different commands require different values that may +// not all be initialisable from the main() function. +func BindToProvider(provider interface{}) Option { + return OptionFunc(func(k *Kong) error { + pv := reflect.ValueOf(provider) + t := pv.Type() + if t.Kind() != reflect.Func || t.NumIn() != 0 || t.NumOut() != 2 || t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { + return errors.Errorf("%T must be a function with the signature func()(T, error)", provider) + } + rt := pv.Type().Out(0) + k.bindings[rt] = func() (reflect.Value, error) { + out := pv.Call(nil) + errv := out[1] + var err error + if !errv.IsNil() { + err = errv.Interface().(error) + } + return out[0], err + } return nil }) } diff --git a/options_test.go b/options_test.go index 5e2da7b..0924e50 100644 --- a/options_test.go +++ b/options_test.go @@ -41,3 +41,29 @@ func TestBindTo(t *testing.T) { require.NoError(t, err) require.Equal(t, "foo", saw) } + +type bindToProviderCLI struct { + Called bool + Cmd bindToProviderCmd `cmd:""` +} + +type boundThing struct { +} + +type bindToProviderCmd struct{} + +func (*bindToProviderCmd) Run(cli *bindToProviderCLI, b *boundThing) error { + cli.Called = true + return nil +} + +func TestBindToProvider(t *testing.T) { + var cli bindToProviderCLI + app, err := New(&cli, BindToProvider(func() (*boundThing, error) { return &boundThing{}, nil })) + require.NoError(t, err) + ctx, err := app.Parse([]string{"cmd"}) + require.NoError(t, err) + err = ctx.Run() + require.NoError(t, err) + require.True(t, cli.Called) +}