Fix enum's from envars not validating (fixes #107).

Also added a mapper for `*os.File`.
This commit is contained in:
Alec Thomas
2020-09-08 13:09:04 +10:00
parent cbae65d227
commit 88ecc9c4e9
7 changed files with 64 additions and 7 deletions
+11 -1
View File
@@ -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:"[<key>]:[<value>]"` 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)
+3 -1
View File
@@ -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
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"`
+28 -2
View File
@@ -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 {
+18
View File
@@ -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)
}
+1
View File
@@ -0,0 +1 @@
Hello world.