From b2cab08684176e7d0a30717e3a159a49377f386b Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 29 Jun 2018 12:33:50 +1000 Subject: [PATCH] Add support for a mapper interface directly on fields. If a field implements the MapperValue interface that interface will be used. --- README.md | 10 ++++++++-- build.go | 4 ++-- mapper.go | 52 +++++++++++++++++++++++++++++++++++++++++--------- mapper_test.go | 19 ++++++++++++++++++ 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 257f9a3..224c205 100644 --- a/README.md +++ b/README.md @@ -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:""`. They are registered with the option @@ -324,6 +325,11 @@ specifies the element type. For maps, the tag has the format `tag:"[]:[]"` 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: diff --git a/build.go b/build.go index e8e36b1..e607617 100644 --- a/build.go +++ b/build.go @@ -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 diff --git a/mapper.go b/mapper.go index d5f04ff..ba40b12 100644 --- a/mapper.go +++ b/mapper.go @@ -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 { diff --git a/mapper_test.go b/mapper_test.go index 12e982f..ce43564 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -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) +}