Add support for a mapper interface directly on fields.
If a field implements the MapperValue interface that interface will be used.
This commit is contained in:
@@ -16,7 +16,8 @@
|
|||||||
1. [Terminating positional arguments](#terminating-positional-arguments)
|
1. [Terminating positional arguments](#terminating-positional-arguments)
|
||||||
1. [Slices](#slices)
|
1. [Slices](#slices)
|
||||||
1. [Maps](#maps)
|
1. [Maps](#maps)
|
||||||
1. [Custom named types](#custom-named-types)
|
1. [Custom named decoders](#custom-named-decoders)
|
||||||
|
1. [Custom decoders](#custom-decoders)
|
||||||
1. [Supported tags](#supported-tags)
|
1. [Supported tags](#supported-tags)
|
||||||
1. [Variable interpolation](#variable-interpolation)
|
1. [Variable interpolation](#variable-interpolation)
|
||||||
1. [Modifying Kong's behaviour](#modifying-kongs-behaviour)
|
1. [Modifying Kong's behaviour](#modifying-kongs-behaviour)
|
||||||
@@ -306,7 +307,7 @@ var CLI struct {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom named types
|
## Custom named decoders
|
||||||
|
|
||||||
Kong includes a number of builtin custom type mappers. These can be used by
|
Kong includes a number of builtin custom type mappers. These can be used by
|
||||||
specifying the tag `type:"<type>"`. They are registered with the option
|
specifying the tag `type:"<type>"`. They are registered with the option
|
||||||
@@ -324,6 +325,11 @@ specifies the element type. For maps, the tag has the format
|
|||||||
`tag:"[<key>]:[<value>]"` where either may be omitted.
|
`tag:"[<key>]:[<value>]"` where either may be omitted.
|
||||||
|
|
||||||
|
|
||||||
|
## Custom decoders
|
||||||
|
|
||||||
|
If a field implements the [MapperValue](https://godoc.org/github.com/alecthomas/kong#MapperValue)
|
||||||
|
interface it will be used to decode arguments into the field.
|
||||||
|
|
||||||
## Supported tags
|
## Supported tags
|
||||||
|
|
||||||
Tags can be in two forms:
|
Tags can be in two forms:
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool
|
|||||||
name = strings.ToLower(dashedString(ft.Name))
|
name = strings.ToLower(dashedString(ft.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nested structs are either commands or args.
|
// Nested structs are either commands or args, unless they implement the Mapper interface.
|
||||||
if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) {
|
if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil {
|
||||||
typ := CommandNode
|
typ := CommandNode
|
||||||
if tag.Arg {
|
if tag.Arg {
|
||||||
typ = ArgumentNode
|
typ = ArgumentNode
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mapperValueType = reflect.TypeOf((*MapperValue)(nil)).Elem()
|
||||||
|
boolMapperType = reflect.TypeOf((*BoolMapper)(nil)).Elem()
|
||||||
|
)
|
||||||
|
|
||||||
// DecodeContext is passed to a Mapper's Decode().
|
// DecodeContext is passed to a Mapper's Decode().
|
||||||
//
|
//
|
||||||
// It contains the Value being decoded into and the Scanner to parse from.
|
// It contains the Value being decoded into and the Scanner to parse from.
|
||||||
@@ -29,17 +34,40 @@ func (r *DecodeContext) WithScanner(scan *Scanner) *DecodeContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MapperValue may be implemented by fields in order to provide custom mapping.
|
||||||
|
type MapperValue interface {
|
||||||
|
Decode(ctx *DecodeContext) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type mapperValueAdapter struct {
|
||||||
|
isBool bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mapperValueAdapter) Decode(ctx *DecodeContext, target reflect.Value) error {
|
||||||
|
if target.Type().Implements(mapperValueType) {
|
||||||
|
return target.Interface().(MapperValue).Decode(ctx)
|
||||||
|
}
|
||||||
|
return target.Addr().Interface().(MapperValue).Decode(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mapperValueAdapter) IsBool() bool {
|
||||||
|
return m.isBool
|
||||||
|
}
|
||||||
|
|
||||||
// A Mapper represents how a field is mapped from command-line values to Go.
|
// A Mapper represents how a field is mapped from command-line values to Go.
|
||||||
//
|
//
|
||||||
// Mappers can be associated with concrete fields via pointer, reflect.Type, reflect.Kind, or via a "type" tag.
|
// Mappers can be associated with concrete fields via pointer, reflect.Type, reflect.Kind, or via a "type" tag.
|
||||||
|
//
|
||||||
|
// Additionally, if a type implements this interface, it will be used.
|
||||||
type Mapper interface {
|
type Mapper interface {
|
||||||
// Decode ctx.Value with ctx.Scanner into target.
|
// Decode ctx.Value with ctx.Scanner into target.
|
||||||
Decode(ctx *DecodeContext, target reflect.Value) error
|
Decode(ctx *DecodeContext, target reflect.Value) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// A BoolMapper is a Mapper to a value that is a boolean.
|
// A BoolMapper is a Mapper to a value that is a boolean.
|
||||||
|
//
|
||||||
|
// This is used solely for formatting help.
|
||||||
type BoolMapper interface {
|
type BoolMapper interface {
|
||||||
Mapper
|
|
||||||
IsBool() bool
|
IsBool() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +106,14 @@ func (r *Registry) ForNamedValue(name string, value reflect.Value) Mapper {
|
|||||||
return r.ForValue(value)
|
return r.ForValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForValue looks up the Mapper for a reflect.Value.
|
||||||
|
func (r *Registry) ForValue(value reflect.Value) Mapper {
|
||||||
|
if mapper, ok := r.values[value]; ok {
|
||||||
|
return mapper
|
||||||
|
}
|
||||||
|
return r.ForType(value.Type())
|
||||||
|
}
|
||||||
|
|
||||||
// ForNamedType finds a mapper for a type with a user-specified name.
|
// ForNamedType finds a mapper for a type with a user-specified name.
|
||||||
//
|
//
|
||||||
// Will return nil if a mapper can not be determined.
|
// Will return nil if a mapper can not be determined.
|
||||||
@@ -88,18 +124,16 @@ func (r *Registry) ForNamedType(name string, typ reflect.Type) Mapper {
|
|||||||
return r.ForType(typ)
|
return r.ForType(typ)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ForValue looks up the Mapper for a reflect.Value.
|
|
||||||
func (r *Registry) ForValue(value reflect.Value) Mapper {
|
|
||||||
if mapper, ok := r.values[value]; ok {
|
|
||||||
return mapper
|
|
||||||
}
|
|
||||||
return r.ForType(value.Type())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForType finds a mapper from a type, by type, then kind.
|
// ForType finds a mapper from a type, by type, then kind.
|
||||||
//
|
//
|
||||||
// Will return nil if a mapper can not be determined.
|
// Will return nil if a mapper can not be determined.
|
||||||
func (r *Registry) ForType(typ reflect.Type) Mapper {
|
func (r *Registry) ForType(typ reflect.Type) Mapper {
|
||||||
|
// Check if the type implements MapperValue.
|
||||||
|
for _, impl := range []reflect.Type{typ, reflect.PtrTo(typ)} {
|
||||||
|
if impl.Implements(mapperValueType) {
|
||||||
|
return &mapperValueAdapter{impl.Implements(boolMapperType)}
|
||||||
|
}
|
||||||
|
}
|
||||||
var mapper Mapper
|
var mapper Mapper
|
||||||
var ok bool
|
var ok bool
|
||||||
if mapper, ok = r.types[typ]; ok {
|
if mapper, ok = r.types[typ]; ok {
|
||||||
|
|||||||
@@ -120,3 +120,22 @@ func TestSliceConsumesRemainingPositionalArgs(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, []string{"ls", "-lart"}, cli.Remainder)
|
require.Equal(t, []string{"ls", "-lart"}, cli.Remainder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mappedValue struct {
|
||||||
|
decoded string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mappedValue) Decode(ctx *kong.DecodeContext) error {
|
||||||
|
m.decoded = ctx.Scan.PopValue("mapped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapperValue(t *testing.T) {
|
||||||
|
var cli struct {
|
||||||
|
Value mappedValue `arg:""`
|
||||||
|
}
|
||||||
|
p := mustNew(t, &cli)
|
||||||
|
_, err := p.Parse([]string{"foo"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "foo", cli.Value.decoded)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user