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:
Alec Thomas
2018-06-29 12:33:50 +10:00
parent 0bc26a865d
commit b2cab08684
4 changed files with 72 additions and 13 deletions
+8 -2
View File
@@ -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:
+2 -2
View File
@@ -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
+43 -9
View File
@@ -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 {
+19
View File
@@ -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)
}