feat: Embed() option and Context.Call()

The former allows arbitrary structs to be embedded in the root of the
CLI, with optional tags.

The latter allows an arbitrary function to be called using Kong's
binding functionality.
This commit is contained in:
Alec Thomas
2022-11-22 23:30:10 +11:00
parent d974d7270a
commit bf0cbf5d7c
7 changed files with 126 additions and 29 deletions
+19 -20
View File
@@ -25,7 +25,7 @@ func build(k *Kong, ast interface{}) (app *Application, err error) {
seenFlags[flag.Name] = true seenFlags[flag.Name] = true
} }
node, err := buildNode(k, iv, ApplicationNode, seenFlags) node, err := buildNode(k, iv, ApplicationNode, newEmptyTag(), seenFlags)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -49,7 +49,7 @@ type flattenedField struct {
tag *Tag tag *Tag
} }
func flattenedFields(v reflect.Value) (out []flattenedField, err error) { func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err error) {
v = reflect.Indirect(v) v = reflect.Indirect(v)
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
ft := v.Type().Field(i) ft := v.Type().Field(i)
@@ -61,6 +61,15 @@ func flattenedFields(v reflect.Value) (out []flattenedField, err error) {
if tag.Ignored { if tag.Ignored {
continue continue
} }
// Assign group if it's not already set.
if tag.Group == "" {
tag.Group = ptag.Group
}
// Accumulate prefixes.
tag.Prefix = ptag.Prefix + tag.Prefix
tag.EnvPrefix = ptag.EnvPrefix + tag.EnvPrefix
// Combine parent vars.
tag.Vars = ptag.Vars.CloneWith(tag.Vars)
// Command and embedded structs can be pointers, so we hydrate them now. // Command and embedded structs can be pointers, so we hydrate them now.
if (tag.Cmd || tag.Embed) && ft.Type.Kind() == reflect.Ptr { if (tag.Cmd || tag.Embed) && ft.Type.Kind() == reflect.Ptr {
fv = reflect.New(ft.Type.Elem()).Elem() fv = reflect.New(ft.Type.Elem()).Elem()
@@ -68,7 +77,8 @@ func flattenedFields(v reflect.Value) (out []flattenedField, err error) {
} }
if !ft.Anonymous && !tag.Embed { if !ft.Anonymous && !tag.Embed {
if fv.CanSet() { if fv.CanSet() {
out = append(out, flattenedField{field: ft, value: fv, tag: tag}) field := flattenedField{field: ft, value: fv, tag: tag}
out = append(out, field)
} }
continue continue
} }
@@ -78,7 +88,7 @@ func flattenedFields(v reflect.Value) (out []flattenedField, err error) {
fv = fv.Elem() fv = fv.Elem()
} else if fv.Type() == reflect.TypeOf(Plugins{}) { } else if fv.Type() == reflect.TypeOf(Plugins{}) {
for i := 0; i < fv.Len(); i++ { for i := 0; i < fv.Len(); i++ {
fields, ferr := flattenedFields(fv.Index(i).Elem()) fields, ferr := flattenedFields(fv.Index(i).Elem(), tag)
if ferr != nil { if ferr != nil {
return nil, ferr return nil, ferr
} }
@@ -86,21 +96,10 @@ func flattenedFields(v reflect.Value) (out []flattenedField, err error) {
} }
continue continue
} }
sub, err := flattenedFields(fv) sub, err := flattenedFields(fv, tag)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, subf := range sub {
// Assign parent if it's not already set.
if subf.tag.Group == "" {
subf.tag.Group = tag.Group
}
// Accumulate prefixes.
subf.tag.Prefix = tag.Prefix + subf.tag.Prefix
subf.tag.EnvPrefix = tag.EnvPrefix + subf.tag.EnvPrefix
// Combine parent vars.
subf.tag.Vars = tag.Vars.CloneWith(subf.tag.Vars)
}
out = append(out, sub...) out = append(out, sub...)
} }
return out, nil return out, nil
@@ -109,13 +108,13 @@ func flattenedFields(v reflect.Value) (out []flattenedField, err error) {
// Build a Node in the Kong data model. // Build a Node in the Kong data model.
// //
// "v" is the value to create the node from, "typ" is the output Node type. // "v" is the value to create the node from, "typ" is the output Node type.
func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool) (*Node, error) { func buildNode(k *Kong, v reflect.Value, typ NodeType, tag *Tag, seenFlags map[string]bool) (*Node, error) {
node := &Node{ node := &Node{
Type: typ, Type: typ,
Target: v, Target: v,
Tag: newEmptyTag(), Tag: tag,
} }
fields, err := flattenedFields(v) fields, err := flattenedFields(v, tag)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -201,7 +200,7 @@ func validatePositionalArguments(node *Node) error {
} }
func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error { func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error {
child, err := buildNode(k, fv, typ, seenFlags) child, err := buildNode(k, fv, typ, newEmptyTag(), seenFlags)
if err != nil { if err != nil {
return err return err
} }
+40 -3
View File
@@ -74,11 +74,14 @@ func getMethod(value reflect.Value, name string) reflect.Value {
return method return method
} }
func callMethod(name string, v, f reflect.Value, bindings bindings) error { func callFunction(f reflect.Value, bindings bindings) error {
if f.Kind() != reflect.Func {
return fmt.Errorf("expected function, got %s", f.Type())
}
in := []reflect.Value{} in := []reflect.Value{}
t := f.Type() t := f.Type()
if t.NumOut() != 1 || !t.Out(0).Implements(callbackReturnSignature) { if t.NumOut() != 1 || !t.Out(0).Implements(callbackReturnSignature) {
return fmt.Errorf("return value of %T.%s() must implement \"error\"", v.Type(), name) return fmt.Errorf("return value of %s must implement \"error\"", t)
} }
for i := 0; i < t.NumIn(); i++ { for i := 0; i < t.NumIn(); i++ {
pt := t.In(i) pt := t.In(i)
@@ -89,7 +92,7 @@ func callMethod(name string, v, f reflect.Value, bindings bindings) error {
} }
in = append(in, argv) in = append(in, argv)
} else { } 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) return fmt.Errorf("couldn't find binding of type %s for parameter %d of %s(), use kong.Bind(%s)", pt, i, t, pt)
} }
} }
out := f.Call(in) out := f.Call(in)
@@ -98,3 +101,37 @@ func callMethod(name string, v, f reflect.Value, bindings bindings) error {
} }
return out[0].Interface().(error) // nolint return out[0].Interface().(error) // nolint
} }
func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) {
if f.Kind() != reflect.Func {
return nil, fmt.Errorf("expected function, got %s", f.Type())
}
in := []reflect.Value{}
t := f.Type()
for i := 0; i < t.NumIn(); i++ {
pt := t.In(i)
if argf, ok := bindings[pt]; ok {
argv, err := argf()
if err != nil {
return nil, err
}
in = append(in, argv)
} else {
return nil, fmt.Errorf("couldn't find binding of type %s for parameter %d of %s(), use kong.Bind(%s)", pt, i, t, pt)
}
}
outv := f.Call(in)
out = make([]any, len(outv))
for i, v := range outv {
out[i] = v.Interface()
}
return out, nil
}
func callMethod(name string, v, f reflect.Value, bindings bindings) error {
err := callFunction(f, bindings)
if err != nil {
return fmt.Errorf("%s.%s(): %w", v.Type(), name, err)
}
return nil
}
+8 -1
View File
@@ -110,7 +110,7 @@ func (c *Context) Bind(args ...interface{}) {
// //
// This will typically have to be called like so: // This will typically have to be called like so:
// //
// BindTo(impl, (*MyInterface)(nil)) // BindTo(impl, (*MyInterface)(nil))
func (c *Context) BindTo(impl, iface interface{}) { func (c *Context) BindTo(impl, iface interface{}) {
c.bindings.addTo(impl, iface) c.bindings.addTo(impl, iface)
} }
@@ -719,6 +719,13 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
return findPotentialCandidates(match, candidates, "unknown flag %s", match) return findPotentialCandidates(match, candidates, "unknown flag %s", match)
} }
// Call an arbitrary function filling arguments with bound values.
func (c *Context) Call(fn any, binds ...interface{}) (out []interface{}, err error) {
fv := reflect.ValueOf(fn)
bindings := c.Kong.bindings.clone().add(binds...).add(c).merge(c.bindings) //nolint:govet
return callAnyFunction(fv, bindings)
}
// RunNode calls the Run() method on an arbitrary node. // RunNode calls the Run() method on an arbitrary node.
// //
// This is useful in conjunction with Visit(), for dynamically running commands. // This is useful in conjunction with Visit(), for dynamically running commands.
+24
View File
@@ -68,6 +68,7 @@ type Kong struct {
// Set temporarily by Options. These are applied after build(). // Set temporarily by Options. These are applied after build().
postBuildOptions []Option postBuildOptions []Option
embedded []embedded
dynamicCommands []*dynamicCommand dynamicCommands []*dynamicCommand
} }
@@ -110,6 +111,25 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
k.Model = model k.Model = model
k.Model.HelpFlag = k.helpFlag k.Model.HelpFlag = k.helpFlag
// Embed any embedded structs.
for _, embed := range k.embedded {
tag, err := parseTagString(strings.Join(embed.tags, " ")) //nolint:govet
if err != nil {
return nil, err
}
tag.Embed = true
v := reflect.Indirect(reflect.ValueOf(embed.strct))
node, err := buildNode(k, v, CommandNode, tag, map[string]bool{})
if err != nil {
return nil, err
}
for _, child := range node.Children {
child.Parent = k.Model.Node
k.Model.Children = append(k.Model.Children, child)
}
k.Model.Flags = append(k.Model.Flags, node.Flags...)
}
// Synthesise command nodes. // Synthesise command nodes.
for _, dcmd := range k.dynamicCommands { for _, dcmd := range k.dynamicCommands {
tag, terr := parseTagString(strings.Join(dcmd.tags, " ")) tag, terr := parseTagString(strings.Join(dcmd.tags, " "))
@@ -188,6 +208,10 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) {
vars = vars.CloneWith(varsContributor.Vars(value)) vars = vars.CloneWith(varsContributor.Vars(value))
} }
if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil {
return fmt.Errorf("enum for %s: %s", value.Summary(), err)
}
updatedVars := map[string]string{ updatedVars := map[string]string{
"default": value.Default, "default": value.Default,
"enum": value.Enum, "enum": value.Enum,
+24 -4
View File
@@ -55,6 +55,25 @@ func Exit(exit func(int)) Option {
}) })
} }
type embedded struct {
strct any
tags []string
}
// Embed a struct into the root of the CLI.
//
// "strct" must be a pointer to a structure.
func Embed(strct any, tags ...string) Option {
t := reflect.TypeOf(strct)
if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
panic("kong: Embed() must be called with a pointer to a struct")
}
return OptionFunc(func(k *Kong) error {
k.embedded = append(k.embedded, embedded{strct, tags})
return nil
})
}
type dynamicCommand struct { type dynamicCommand struct {
name string name string
help string help string
@@ -164,8 +183,8 @@ func Writers(stdout, stderr io.Writer) Option {
// //
// There are two hook points: // There are two hook points:
// //
// BeforeApply(...) error // BeforeApply(...) error
// AfterApply(...) error // AfterApply(...) error
// //
// Called before validation/assignment, and immediately after validation/assignment, respectively. // Called before validation/assignment, and immediately after validation/assignment, respectively.
func Bind(args ...interface{}) Option { func Bind(args ...interface{}) Option {
@@ -177,7 +196,7 @@ func Bind(args ...interface{}) Option {
// BindTo allows binding of implementations to interfaces. // BindTo allows binding of implementations to interfaces.
// //
// BindTo(impl, (*iface)(nil)) // BindTo(impl, (*iface)(nil))
func BindTo(impl, iface interface{}) Option { func BindTo(impl, iface interface{}) Option {
return OptionFunc(func(k *Kong) error { return OptionFunc(func(k *Kong) error {
k.bindings.addTo(impl, iface) k.bindings.addTo(impl, iface)
@@ -428,7 +447,8 @@ func siftStrings(ss []string, filter func(s string) bool) []string {
// Predefined environment variables are skipped. // Predefined environment variables are skipped.
// //
// For example: // For example:
// --some.value -> PREFIX_SOME_VALUE //
// --some.value -> PREFIX_SOME_VALUE
func DefaultEnvars(prefix string) Option { func DefaultEnvars(prefix string) Option {
processFlag := func(flag *Flag) { processFlag := func(flag *Flag) {
switch env := flag.Env; { switch env := flag.Env; {
+1 -1
View File
@@ -58,7 +58,7 @@ func TestInvalidCallback(t *testing.T) {
p, err := New(&cli, BindTo(impl("foo"), (*iface)(nil))) p, err := New(&cli, BindTo(impl("foo"), (*iface)(nil)))
assert.NoError(t, err) assert.NoError(t, err)
err = callMethod("method", reflect.ValueOf(impl("??")), reflect.ValueOf(method), p.bindings) err = callMethod("method", reflect.ValueOf(impl("??")), reflect.ValueOf(method), p.bindings)
assert.EqualError(t, err, `return value of *reflect.rtype.method() must implement "error"`) assert.EqualError(t, err, `kong.impl.method(): return value of func(kong.iface) string must implement "error"`)
} }
type zrror struct{} type zrror struct{}
+10
View File
@@ -44,6 +44,16 @@ type Tag struct {
items map[string][]string items map[string][]string
} }
func (t *Tag) String() string {
out := []string{}
for key, list := range t.items {
for _, value := range list {
out = append(out, fmt.Sprintf("%s:%q", key, value))
}
}
return strings.Join(out, " ")
}
type tagChars struct { type tagChars struct {
sep, quote, assign rune sep, quote, assign rune
} }