From 88ecc9c4e977bd29c4b21b3e83ec56045d549328 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 8 Sep 2020 13:09:04 +1000 Subject: [PATCH] Fix enum's from envars not validating (fixes #107). Also added a mapper for `*os.File`. --- README.md | 12 +++++++++++- context.go | 4 +++- hooks.go | 2 +- kong_test.go | 4 ++-- mapper.go | 30 ++++++++++++++++++++++++++++-- mapper_test.go | 18 ++++++++++++++++++ testdata/file.txt | 1 + 7 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 testdata/file.txt diff --git a/README.md b/README.md index eaad200..e38778f 100644 --- a/README.md +++ b/README.md @@ -382,11 +382,21 @@ Slices and maps treat type tags specially. For slices, the `type:""` tag specifies the element type. For maps, the tag has the format `tag:"[]:[]"` where either may be omitted. +## Supported field types + ## Custom decoders (mappers) + Any field implementing `encoding.TextUnmarshaler` or `json.Unmarshaler` will use those interfaces -for decoding values. +for decoding values. Kong also includes builtin support for many common Go types: + +| Type | Description +|---------------------|-------------------------------------------- +| `time.Duration` | Populated using `time.ParseDuration()`. +| `time.Time` | Populated using `time.Parse()`. Format defaults to RFC3339 but can be overridden with the `format:"X"` tag. +| `*os.File` | Path to a file that will be opened, or `-` for `os.Stdin`. File must be closed by the user. +| `*url.URL` | Populated with `url.Parse()`. For more fine-grained control, if a field implements the [MapperValue](https://godoc.org/github.com/alecthomas/kong#MapperValue) diff --git a/context.go b/context.go index ada529e..56efd92 100644 --- a/context.go +++ b/context.go @@ -2,6 +2,7 @@ package kong import ( "fmt" + "os" "reflect" "sort" "strconv" @@ -136,7 +137,8 @@ func (c *Context) Empty() bool { func (c *Context) Validate() error { // nolint: gocyclo err := Visit(c.Model, func(node Visitable, next Next) error { if value, ok := node.(*Value); ok { - if value.Enum != "" && (!value.Required || value.Default != "") { + _, ok := os.LookupEnv(value.Tag.Env) + if value.Enum != "" && (!value.Required || value.Default != "" || (value.Tag.Env != "" && ok)) { if err := checkEnum(value, value.Target); err != nil { return err } diff --git a/hooks.go b/hooks.go index 08fa171..d166b08 100644 --- a/hooks.go +++ b/hooks.go @@ -1,6 +1,6 @@ package kong -// BeforeResolve is a documentation-only interface describing hooks that run before values are set. +// BeforeResolve is a documentation-only interface describing hooks that run before resolvers are applied. type BeforeResolve interface { // This is not the correct signature - see README for details. BeforeResolve(args ...interface{}) error diff --git a/kong_test.go b/kong_test.go index 40731c3..6c2097a 100644 --- a/kong_test.go +++ b/kong_test.go @@ -743,7 +743,7 @@ func TestEnvarEnumValidated(t *testing.T) { }) defer restore() var cli struct { - Flag string `env:"FLAG" enum:"valid" default:"valid"` + Flag string `env:"FLAG" required:"" enum:"valid"` } p := mustNew(t, &cli) _, err := p.Parse(nil) @@ -798,7 +798,7 @@ func TestIssue40EnumAcrossCommands(t *testing.T) { OneArg string `arg:"" required:""` } `cmd:""` Two struct { - TwoArg string `arg:"" enum:"a,b,c" required:""` + TwoArg string `arg:"" enum:"a,b,c" required:"" env:"FOO"` } `cmd:""` Three struct { ThreeArg string `arg:"" optional:"" default:"a" enum:"a,b,c"` diff --git a/mapper.go b/mapper.go index a42bc5d..875e438 100644 --- a/mapper.go +++ b/mapper.go @@ -231,8 +231,8 @@ func (r *Registry) RegisterDefaults() *Registry { RegisterKind(reflect.Int16, intDecoder(16)). RegisterKind(reflect.Int32, intDecoder(32)). RegisterKind(reflect.Int64, intDecoder(64)). - RegisterKind(reflect.Uint, uintDecoder(64)). - RegisterKind(reflect.Uint8, uintDecoder(bits.UintSize)). + RegisterKind(reflect.Uint, uintDecoder(bits.UintSize)). + RegisterKind(reflect.Uint8, uintDecoder(8)). RegisterKind(reflect.Uint16, uintDecoder(16)). RegisterKind(reflect.Uint32, uintDecoder(32)). RegisterKind(reflect.Uint64, uintDecoder(64)). @@ -248,6 +248,7 @@ func (r *Registry) RegisterDefaults() *Registry { RegisterType(reflect.TypeOf(time.Time{}), timeDecoder()). RegisterType(reflect.TypeOf(time.Duration(0)), durationDecoder()). RegisterType(reflect.TypeOf(&url.URL{}), urlMapper()). + RegisterType(reflect.TypeOf(&os.File{}), fileMapper(r)). RegisterName("path", pathMapper(r)). RegisterName("existingfile", existingFileMapper(r)). RegisterName("existingdir", existingDirMapper(r)). @@ -541,6 +542,31 @@ func pathMapper(r *Registry) MapperFunc { } } +func fileMapper(r *Registry) MapperFunc { + return func(ctx *DecodeContext, target reflect.Value) error { + if target.Kind() == reflect.Slice { + return sliceDecoder(r)(ctx, target) + } + var path string + err := ctx.Scan.PopValueInto("file", &path) + if err != nil { + return err + } + var file *os.File + if path == "-" { + file = os.Stdin + } else { + path = ExpandPath(path) + file, err = os.Open(path) // nolint: gosec + if err != nil { + return err + } + } + target.Set(reflect.ValueOf(file)) + return nil + } +} + func existingFileMapper(r *Registry) MapperFunc { return func(ctx *DecodeContext, target reflect.Value) error { if target.Kind() == reflect.Slice { diff --git a/mapper_test.go b/mapper_test.go index c0ba855..ca4f42f 100644 --- a/mapper_test.go +++ b/mapper_test.go @@ -345,3 +345,21 @@ func TestNumbers(t *testing.T) { }, cli) }) } + +func TestFileMapper(t *testing.T) { + type CLI struct { + File *os.File `arg:""` + } + var cli CLI + p := mustNew(t, &cli) + _, err := p.Parse([]string{"testdata/file.txt"}) + require.NoError(t, err) + require.NotNil(t, cli.File) + _ = cli.File.Close() + _, err = p.Parse([]string{"testdata/missing.txt"}) + require.Error(t, err) + require.Contains(t, err.Error(), "missing.txt: no such file or directory") + _, err = p.Parse([]string{"-"}) + require.NoError(t, err) + require.Equal(t, os.Stdin, cli.File) +} diff --git a/testdata/file.txt b/testdata/file.txt new file mode 100644 index 0000000..4baf047 --- /dev/null +++ b/testdata/file.txt @@ -0,0 +1 @@ +Hello world. \ No newline at end of file