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:
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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{}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user