Decoders are now field mappers.

Mappers are responsible for mapping from command-line input to Go. This
is typically just decoding, but also includes other information such as
if the field is a bool.
This commit is contained in:
Alec Thomas
2018-06-03 16:07:30 +10:00
committed by Gerald Kaszuba
parent c8b487e49c
commit 48af58cefa
9 changed files with 337 additions and 288 deletions
+13 -12
View File
@@ -6,7 +6,7 @@ import (
"strings"
)
func build(ast interface{}, extraFlags []*Flag) (app *Application, err error) {
func build(k *Kong, ast interface{}) (app *Application, err error) {
defer catch(&err)
v := reflect.ValueOf(ast)
iv := reflect.Indirect(v)
@@ -15,11 +15,12 @@ func build(ast interface{}, extraFlags []*Flag) (app *Application, err error) {
}
app = &Application{}
extraFlags := k.extraFlags()
seenFlags := map[string]bool{}
for _, flag := range extraFlags {
seenFlags[flag.Name] = true
}
node := buildNode(iv, ApplicationNode, seenFlags)
node := buildNode(k, iv, ApplicationNode, seenFlags)
if len(node.Positional) > 0 && len(node.Children) > 0 {
return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast)
}
@@ -32,7 +33,7 @@ func dashedString(s string) string {
return strings.Join(camelCase(s), "-")
}
func buildNode(v reflect.Value, typ NodeType, seenFlags map[string]bool) *Node {
func buildNode(k *Kong, v reflect.Value, typ NodeType, seenFlags map[string]bool) *Node {
node := &Node{
Type: typ,
Target: v,
@@ -57,9 +58,9 @@ func buildNode(v reflect.Value, typ NodeType, seenFlags map[string]bool) *Node {
if tag.Arg {
typ = ArgumentNode
}
buildChild(node, typ, v, ft, fv, tag, name, seenFlags)
buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags)
} else {
buildField(node, v, ft, fv, tag, name, seenFlags)
buildField(k, node, v, ft, fv, tag, name, seenFlags)
}
}
@@ -82,8 +83,8 @@ func buildNode(v reflect.Value, typ NodeType, seenFlags map[string]bool) *Node {
return node
}
func buildChild(node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
child := buildNode(fv, typ, seenFlags)
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) {
child := buildNode(k, fv, typ, seenFlags)
child.Parent = node
child.Help = tag.Help
@@ -118,10 +119,10 @@ func buildChild(node *Node, typ NodeType, v reflect.Value, ft reflect.StructFiel
}
}
func buildField(node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
decoder := DecoderForField(tag.Type, ft)
if decoder == nil {
fail("no decoder for %s.%s (of type %s)", v.Type(), ft.Name, ft.Type)
func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) {
mapper := k.registry.ForNamedType(tag.Type, fv)
if mapper == nil {
fail("no mapper for %s.%s (of type %s)", v.Type(), ft.Name, ft.Type)
}
flag := !tag.Arg
@@ -131,7 +132,7 @@ func buildField(node *Node, v reflect.Value, ft reflect.StructField, fv reflect.
Flag: flag,
Help: tag.Help,
Default: tag.Default,
Decoder: decoder,
Mapper: mapper,
Tag: tag,
Value: fv,
-250
View File
@@ -1,250 +0,0 @@
package kong
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
)
type DecoderContext struct {
// Value being decoded into.
Value *Value
}
// A Decoder knows how to decode text into a Go value.
type Decoder interface {
// Decode scan into target.
//
// "ctx" contains context about the value being decoded that may be useful
// to some decoders.
Decode(ctx *DecoderContext, scan *Scanner, target reflect.Value) error
}
type DecoderFunc func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error
func (d DecoderFunc) Decode(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
return d(ctx, scan, target)
}
var _ Decoder = DecoderFunc(nil)
type TypeDecoder interface {
Type() reflect.Type
Decoder
}
func NewTypeDecoder(typ reflect.Type, decoder DecoderFunc) TypeDecoder {
return &typeDecoder{typ, decoder}
}
type typeDecoder struct {
typ reflect.Type
DecoderFunc
}
func (t *typeDecoder) Type() reflect.Type { return t.typ }
var _ TypeDecoder = &typeDecoder{}
type KindDecoder interface {
Kind() reflect.Kind
Decoder
}
func NewKindDecoder(kind reflect.Kind, decoder DecoderFunc) KindDecoder {
return &kindDecoder{kind, decoder}
}
type kindDecoder struct {
kind reflect.Kind
DecoderFunc
}
func (k *kindDecoder) Kind() reflect.Kind { return k.kind }
var _ KindDecoder = &kindDecoder{}
// A NamedDecoder will be used if the value field has a "type" tag matching Name().
//
// eg.
//
// Field string `kong:"type='colour'`
// kong.RegisterDecoder(kong.NewNamedDecoder("colour", ...))
type NamedDecoder interface {
Name() string
Decoder
}
func NewNamedDecoder(name string, decoder DecoderFunc) NamedDecoder {
return &namedDecoder{name, decoder}
}
type namedDecoder struct {
name string
DecoderFunc
}
func (n *namedDecoder) Name() string { return n.name }
var _ NamedDecoder = &namedDecoder{}
var (
namedDecoders = map[string]NamedDecoder{}
typeDecoders = map[reflect.Type]TypeDecoder{}
kindDecoders = map[reflect.Kind]KindDecoder{}
)
// DecoderForField finds a decoder for a struct field.
//
// Will return nil if a decoder can not be determined.
func DecoderForField(name string, field reflect.StructField) Decoder {
if decoder, ok := namedDecoders[name]; ok {
return decoder
}
return DecoderForType(field.Type)
}
// DecoderForType finds a decoder from a type or kind.
//
// Will return nil if a decoder can not be determined.
func DecoderForType(typ reflect.Type) Decoder {
var decoder Decoder
var ok bool
if decoder, ok = typeDecoders[typ]; ok {
return decoder
} else if decoder, ok = kindDecoders[typ.Kind()]; ok {
return decoder
}
return nil
}
// RegisterDecoder registers decoders.
//
// Decoders must be one of TypeDecoder, KindDecoder or NamedDecoder.
func RegisterDecoder(decoders ...Decoder) {
for _, decoder := range decoders {
switch decoder := decoder.(type) {
case TypeDecoder:
typeDecoders[decoder.Type()] = decoder
case KindDecoder:
kindDecoders[decoder.Kind()] = decoder
case NamedDecoder:
namedDecoders[decoder.Name()] = decoder
default:
fail("unsupported decoder type %T", decoder)
}
}
}
func init() {
RegisterDecoder(
NewKindDecoder(reflect.Int, intDecoder),
NewKindDecoder(reflect.Int8, intDecoder),
NewKindDecoder(reflect.Int16, intDecoder),
NewKindDecoder(reflect.Int32, intDecoder),
NewKindDecoder(reflect.Int64, intDecoder),
NewKindDecoder(reflect.Uint, uintDecoder),
NewKindDecoder(reflect.Uint8, uintDecoder),
NewKindDecoder(reflect.Uint16, uintDecoder),
NewKindDecoder(reflect.Uint32, uintDecoder),
NewKindDecoder(reflect.Uint64, uintDecoder),
NewKindDecoder(reflect.Float32, floatDecoder(32)),
NewKindDecoder(reflect.Float64, floatDecoder(64)),
NewKindDecoder(reflect.String, func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
target.SetString(scan.PopValue("string"))
return nil
}),
NewKindDecoder(reflect.Bool, func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
target.SetBool(true)
return nil
}),
NewKindDecoder(reflect.Slice, sliceDecoder),
NewTypeDecoder(reflect.TypeOf(time.Time{}), timeDecoder),
NewTypeDecoder(reflect.TypeOf(time.Duration(0)), durationDecoder),
)
}
func durationDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
d, err := time.ParseDuration(scan.PopValue("duration"))
if err != nil {
return err
}
target.Set(reflect.ValueOf(d))
return nil
}
func timeDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
fmt := time.RFC3339
if ctx.Value.Format != "" {
fmt = ctx.Value.Format
}
t, err := time.Parse(fmt, scan.PopValue("time"))
if err != nil {
return err
}
target.Set(reflect.ValueOf(t))
return nil
}
func intDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
value := scan.PopValue("int")
n, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid int %q", value)
}
target.SetInt(n)
return nil
}
func uintDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
value := scan.PopValue("uint")
n, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid uint %q", value)
}
target.SetUint(n)
return nil
}
func floatDecoder(bits int) DecoderFunc {
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
value := scan.PopValue("float")
n, err := strconv.ParseFloat(value, bits)
if err != nil {
return fmt.Errorf("invalid float %q", value)
}
target.SetFloat(n)
return nil
}
}
func sliceDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
el := target.Type().Elem()
sep, ok := ctx.Value.Tag.Get("sep")
if !ok {
sep = ","
}
var childScanner *Scanner
if ctx.Value.Flag {
// If decoding a flag, we need an argument.
childScanner = Scan(strings.Split(scan.PopValue("list"), sep)...)
} else {
tokens := scan.PopUntil(func(t Token) bool { return !t.IsValue() })
childScanner = Scan(tokens...)
}
childDecoder := DecoderForType(el)
if childDecoder == nil {
return fmt.Errorf("no decoder for element type of %s", target.Type())
}
for childScanner.Peek().Type != EOLToken {
childValue := reflect.New(el).Elem()
err := childDecoder.Decode(ctx, childScanner, childValue)
if err != nil {
return err
}
target.Set(reflect.Append(target, childValue))
}
return nil
}
-6
View File
@@ -1,6 +0,0 @@
package kong
import "testing"
func TestDecoders(t *testing.T) {
}
+2 -2
View File
@@ -82,11 +82,11 @@ func init() {
})
}
// Help returns a Hook that will display help and exit.
// Help returns a Before hook that will display help and exit.
//
// tmpl receives a context with several top-level values, in addition to those passed through tmplctx:
// .Context which is of type *Context and .Path which is of type *Path.
func Help(tmpl *raymond.Template, tmplctx map[string]interface{}) HookFunction {
func Help(tmpl *raymond.Template, tmplctx map[string]interface{}) Before {
return func(ctx *Context, path *Path) error {
merged := map[string]interface{}{
"App": ctx.App,
+21 -13
View File
@@ -8,7 +8,8 @@ import (
"reflect"
)
type HookFunction func(ctx *Context, path *Path) error
// Before is a callback tied to a field of the grammar, called before a value is applied.
type Before func(ctx *Context, path *Path) error
// Error reported by Kong.
type Error struct{ msg string }
@@ -38,7 +39,8 @@ type Kong struct {
Stdout io.Writer
Stderr io.Writer
hooks map[reflect.Value]HookFunction
before map[reflect.Value]Before
registry *Registry
noDefaultHelp bool
}
@@ -47,13 +49,18 @@ type Kong struct {
// See the README (https://github.com/alecthomas/kong) for usage instructions.
func New(grammar interface{}, options ...Option) (*Kong, error) {
k := &Kong{
Exit: os.Exit,
Stdout: os.Stdout,
Stderr: os.Stderr,
hooks: map[reflect.Value]HookFunction{},
Exit: os.Exit,
Stdout: os.Stdout,
Stderr: os.Stderr,
before: map[reflect.Value]Before{},
registry: NewRegistry().RegisterDefaults(),
}
model, err := build(grammar, k.extraFlags())
for _, option := range options {
option(k)
}
model, err := build(k, grammar)
if err != nil {
return k, err
}
@@ -72,13 +79,14 @@ func (k *Kong) extraFlags() []*Flag {
return nil
}
helpValue := false
value := reflect.ValueOf(&helpValue).Elem()
helpFlag := &Flag{
Value: Value{
Name: "help",
Help: "Show context-sensitive help.",
Flag: true,
Value: reflect.ValueOf(&helpValue).Elem(),
Decoder: kindDecoders[reflect.Bool],
Name: "help",
Help: "Show context-sensitive help.",
Flag: true,
Value: value,
Mapper: k.registry.ForValue(value),
},
}
hook := Hook(&helpValue, Help(defaultHelpTemplate, nil))
@@ -133,7 +141,7 @@ func (k *Kong) applyHooks(ctx *Context) error {
if key.IsValid() {
key = key.Addr()
}
if hook := k.hooks[key]; hook != nil {
if hook := k.before[key]; hook != nil {
if err := hook(ctx, trace); err != nil {
return err
}
+229
View File
@@ -0,0 +1,229 @@
package kong
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
)
type DecoderContext struct {
// Value being decoded into.
Value *Value
}
// 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.
type Mapper interface {
Decode(ctx *DecoderContext, scan *Scanner, target reflect.Value) error
}
// A BoolMapper is a Mapper to a value that is a boolean.
type BoolMapper interface {
Mapper
IsBool() bool
}
type MapperFunc func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error
func (d MapperFunc) Decode(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
return d(ctx, scan, target)
}
// A Registry encapsulates a set of fields and lookups to resolve them.
type Registry struct {
names map[string]Mapper
types map[reflect.Type]Mapper
kinds map[reflect.Kind]Mapper
values map[reflect.Value]Mapper
}
func NewRegistry() *Registry {
return &Registry{
names: map[string]Mapper{},
types: map[reflect.Type]Mapper{},
kinds: map[reflect.Kind]Mapper{},
values: map[reflect.Value]Mapper{},
}
}
// ForNamedType finds a mapper for a value with a user-specified type.
//
// Will return nil if a mapper can not be determined.
func (d *Registry) ForNamedType(name string, value reflect.Value) Mapper {
if mapper, ok := d.names[name]; ok {
return mapper
}
return d.ForValue(value)
}
func (d *Registry) ForValue(value reflect.Value) Mapper {
if mapper, ok := d.values[value]; ok {
return mapper
}
return d.ForType(value.Type())
}
// DecoderForType finds a mapper from a type or kind.
//
// Will return nil if a mapper can not be determined.
func (d *Registry) ForType(typ reflect.Type) Mapper {
var mapper Mapper
var ok bool
if mapper, ok = d.types[typ]; ok {
return mapper
} else if mapper, ok = d.kinds[typ.Kind()]; ok {
return mapper
}
return nil
}
func (d *Registry) RegisterKind(kind reflect.Kind, mapper Mapper) *Registry {
d.kinds[kind] = mapper
return d
}
// RegisterName registeres a mapper to be used if the value mapper has a "type" tag matching name.
//
// eg.
//
// Mapper string `kong:"type='colour'`
// registry.RegisterName("colour", ...)
func (d *Registry) RegisterName(name string, mapper Mapper) *Registry {
d.names[name] = mapper
return d
}
func (d *Registry) RegisterType(typ reflect.Type, mapper Mapper) *Registry {
d.types[typ] = mapper
return d
}
// RegisterValue registers a mapper by a pointer to the mapper value.
func (d *Registry) RegisterValue(ptr interface{}, mapper Mapper) *Registry {
key := reflect.ValueOf(ptr)
if key.Kind() != reflect.Ptr {
panic("expected a pointer")
}
key = key.Elem()
d.values[key] = mapper
return d
}
func (d *Registry) RegisterDefaults() *Registry {
return d.RegisterKind(reflect.Int, MapperFunc(intDecoder)).
RegisterKind(reflect.Int8, MapperFunc(intDecoder)).
RegisterKind(reflect.Int16, MapperFunc(intDecoder)).
RegisterKind(reflect.Int32, MapperFunc(intDecoder)).
RegisterKind(reflect.Int64, MapperFunc(intDecoder)).
RegisterKind(reflect.Uint, MapperFunc(uintDecoder)).
RegisterKind(reflect.Uint8, MapperFunc(uintDecoder)).
RegisterKind(reflect.Uint16, MapperFunc(uintDecoder)).
RegisterKind(reflect.Uint32, MapperFunc(uintDecoder)).
RegisterKind(reflect.Uint64, MapperFunc(uintDecoder)).
RegisterKind(reflect.Float32, floatDecoder(32)).
RegisterKind(reflect.Float64, floatDecoder(64)).
RegisterKind(reflect.String, MapperFunc(func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
target.SetString(scan.PopValue("string"))
return nil
})).
RegisterKind(reflect.Bool, boolMapper{}).
RegisterType(reflect.TypeOf(time.Time{}), MapperFunc(timeDecoder)).
RegisterType(reflect.TypeOf(time.Duration(0)), MapperFunc(durationDecoder)).
RegisterKind(reflect.Slice, sliceDecoder(d))
}
type boolMapper struct{}
func (boolMapper) Decode(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
target.SetBool(true)
return nil
}
func (boolMapper) IsBool() bool { return true }
func durationDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
d, err := time.ParseDuration(scan.PopValue("duration"))
if err != nil {
return err
}
target.Set(reflect.ValueOf(d))
return nil
}
func timeDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
fmt := time.RFC3339
if ctx.Value.Format != "" {
fmt = ctx.Value.Format
}
t, err := time.Parse(fmt, scan.PopValue("time"))
if err != nil {
return err
}
target.Set(reflect.ValueOf(t))
return nil
}
func intDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
value := scan.PopValue("int")
n, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid int %q", value)
}
target.SetInt(n)
return nil
}
func uintDecoder(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
value := scan.PopValue("uint")
n, err := strconv.ParseUint(value, 10, 64)
if err != nil {
return fmt.Errorf("invalid uint %q", value)
}
target.SetUint(n)
return nil
}
func floatDecoder(bits int) MapperFunc {
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
value := scan.PopValue("float")
n, err := strconv.ParseFloat(value, bits)
if err != nil {
return fmt.Errorf("invalid float %q", value)
}
target.SetFloat(n)
return nil
}
}
func sliceDecoder(d *Registry) MapperFunc {
return func(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
el := target.Type().Elem()
sep, ok := ctx.Value.Tag.Get("sep")
if !ok {
sep = ","
}
var childScanner *Scanner
if ctx.Value.Flag {
// If decoding a flag, we need an argument.
childScanner = Scan(strings.Split(scan.PopValue("list"), sep)...)
} else {
tokens := scan.PopUntil(func(t Token) bool { return !t.IsValue() })
childScanner = Scan(tokens...)
}
childDecoder := d.ForType(el)
if childDecoder == nil {
return fmt.Errorf("no mapper for element type of %s", target.Type())
}
for childScanner.Peek().Type != EOLToken {
childValue := reflect.New(el).Elem()
err := childDecoder.Decode(ctx, childScanner, childValue)
if err != nil {
return err
}
target.Set(reflect.Append(target, childValue))
}
return nil
}
}
+42
View File
@@ -0,0 +1,42 @@
package kong
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
func TestValueMapper(t *testing.T) {
var cli struct {
Flag string
}
k := mustNew(t, &cli, ValueMapper(&cli.Flag, testMooMapper{}))
_, err := k.Parse(nil)
require.NoError(t, err)
require.Equal(t, "", cli.Flag)
_, err = k.Parse([]string{"--flag"})
require.NoError(t, err)
require.Equal(t, "MOO", cli.Flag)
}
func TestNamedMapper(t *testing.T) {
var cli struct {
Flag string `type:"moo"`
}
k := mustNew(t, &cli, NamedMapper("moo", testMooMapper{}))
_, err := k.Parse(nil)
require.NoError(t, err)
require.Equal(t, "", cli.Flag)
_, err = k.Parse([]string{"--flag"})
require.NoError(t, err)
require.Equal(t, "MOO", cli.Flag)
}
type testMooMapper struct{}
func (testMooMapper) Decode(ctx *DecoderContext, scan *Scanner, target reflect.Value) error {
target.SetString("MOO")
return nil
}
func (testMooMapper) IsBool() bool { return true }
+5 -2
View File
@@ -130,7 +130,7 @@ type Value struct {
Name string
Help string
Default string
Decoder Decoder
Mapper Mapper
Tag *Tag
Value reflect.Value
Required bool
@@ -144,13 +144,16 @@ func (v *Value) IsCumulative() bool {
}
func (v *Value) IsBool() bool {
if m, ok := v.Mapper.(BoolMapper); ok && m.IsBool() {
return true
}
return v.Value.Kind() == reflect.Bool
}
// Parse tokens into value, parse, and validate, but do not write to the field.
func (v *Value) Parse(scan *Scanner) (reflect.Value, error) {
value := reflect.New(v.Value.Type()).Elem()
err := v.Decoder.Decode(&DecoderContext{Value: v}, scan, value)
err := v.Mapper.Decode(&DecoderContext{Value: v}, scan, value)
if err == nil {
v.Set = true
}
+25 -3
View File
@@ -32,6 +32,26 @@ func Name(name string) Option {
}
}
// TypeMapper registers a mapper to a type.
func TypeMapper(typ reflect.Type, mapper Mapper) Option {
return func(k *Kong) { k.registry.RegisterType(typ, mapper) }
}
// KindMapper registers a mapper to a kind.
func KindMapper(kind reflect.Kind, mapper Mapper) Option {
return func(k *Kong) { k.registry.RegisterKind(kind, mapper) }
}
// ValueMapper registers a mapper to a field value.
func ValueMapper(ptr interface{}, mapper Mapper) Option {
return func(k *Kong) { k.registry.RegisterValue(ptr, mapper) }
}
// NamedMapper registers a mapper to a name.
func NamedMapper(name string, mapper Mapper) Option {
return func(k *Kong) { k.registry.RegisterName(name, mapper) }
}
// Description sets the application description.
func Description(description string) Option {
return func(k *Kong) {
@@ -49,13 +69,15 @@ func Writers(stdout, stderr io.Writer) Option {
}
}
// Hook to execute when a command, flag or positional argument is encountered.
func Hook(ptr interface{}, hook HookFunction) Option {
// Hook to aply before a command, flag or positional argument is encountered.
//
// "ptr" is a pointer to a field of the grammar.
func Hook(ptr interface{}, hook Before) Option {
key := reflect.ValueOf(ptr)
if key.Kind() != reflect.Ptr {
panic("expected a pointer")
}
return func(k *Kong) {
k.hooks[key] = hook
k.before[key] = hook
}
}