Fix enum's from envars not validating (fixes #107).
Also added a mapper for `*os.File`.
This commit is contained in:
@@ -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
@@ -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,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
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
Hello world.
|
||||
Reference in New Issue
Block a user