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. [Slices](#slices)
|
||||
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. [Variable interpolation](#variable-interpolation)
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
// Nested structs are either commands or args.
|
||||
if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) {
|
||||
// Nested structs are either commands or args, unless they implement the Mapper interface.
|
||||
if ft.Type.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil {
|
||||
typ := CommandNode
|
||||
if tag.Arg {
|
||||
typ = ArgumentNode
|
||||
|
||||
@@ -11,6 +11,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
mapperValueType = reflect.TypeOf((*MapperValue)(nil)).Elem()
|
||||
boolMapperType = reflect.TypeOf((*BoolMapper)(nil)).Elem()
|
||||
)
|
||||
|
||||
// DecodeContext is passed to a Mapper's Decode().
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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 {
|
||||
// Decode ctx.Value with ctx.Scanner into target.
|
||||
Decode(ctx *DecodeContext, target reflect.Value) error
|
||||
}
|
||||
|
||||
// A BoolMapper is a Mapper to a value that is a boolean.
|
||||
//
|
||||
// This is used solely for formatting help.
|
||||
type BoolMapper interface {
|
||||
Mapper
|
||||
IsBool() bool
|
||||
}
|
||||
|
||||
@@ -78,6 +106,14 @@ func (r *Registry) ForNamedValue(name string, value reflect.Value) Mapper {
|
||||
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.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Will return nil if a mapper can not be determined.
|
||||
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 ok bool
|
||||
if mapper, ok = r.types[typ]; ok {
|
||||
|
||||
@@ -120,3 +120,22 @@ func TestSliceConsumesRemainingPositionalArgs(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
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