From 0983e49ca286e486e79ee7466e175a8c8d0126c4 Mon Sep 17 00:00:00 2001 From: Scott Wisniewski Date: Sat, 12 Mar 2022 18:30:59 -0800 Subject: [PATCH] Add support for pointer fields. --- README.md | 14 +++++++ kong_test.go | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++ mapper.go | 19 +++++++++- 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 372db71..3a172ea 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,20 @@ var CLI struct { For flags, multiple key+value pairs should be separated by `mapsep:"rune"` tag (defaults to `;`) eg. `--set="key1=value1;key2=value2"`. +## Pointers + +Pointers work like the underlying type, except that you can differentiate between the presence of the zero value and no value being supplied. + +For example: + +```go +var CLI struct { + Foo *int +} +``` + +Would produce a nil value for `Foo` if no `--foo` argument is supplied, but would have a pointer to the value 0 if the argument `--foo=0` was supplied. + ## Nested data structure Kong support a nested data structure as well with `embed:""`. You can combine `embed:""` with `prefix:""`: diff --git a/kong_test.go b/kong_test.go index 6ce73f5..ec53ed7 100644 --- a/kong_test.go +++ b/kong_test.go @@ -1542,3 +1542,106 @@ func TestPassthroughCmdOnlyStringArgs(t *testing.T) { _, err := kong.New(&cli) require.EqualError(t, err, ".Command: passthrough command command [ ...] must contain exactly one positional argument of []string type") } + +func TestStringPointer(t *testing.T) { + var cli struct { + Foo *string + } + k, err := kong.New(&cli) + require.NoError(t, err) + require.NotNil(t, k) + ctx, err := k.Parse([]string{"--foo", "wtf"}) + require.NoError(t, err) + require.NotNil(t, ctx) + require.NotNil(t, cli.Foo) + require.Equal(t, "wtf", *cli.Foo) +} + +func TestStringPointerNoValue(t *testing.T) { + var cli struct { + Foo *string + } + k, err := kong.New(&cli) + require.NoError(t, err) + require.NotNil(t, k) + ctx, err := k.Parse([]string{}) + require.NoError(t, err) + require.NotNil(t, ctx) + require.Nil(t, cli.Foo) +} + +func TestStringPointerDefault(t *testing.T) { + var cli struct { + Foo *string `default:"stuff"` + } + k, err := kong.New(&cli) + require.NoError(t, err) + require.NotNil(t, k) + ctx, err := k.Parse([]string{}) + require.NoError(t, err) + require.NotNil(t, ctx) + require.NotNil(t, cli.Foo) + require.Equal(t, "stuff", *cli.Foo) +} + +func TestStringPointerAliasNoValue(t *testing.T) { + type Foo string + var cli struct { + F *Foo + } + k, err := kong.New(&cli) + require.NoError(t, err) + require.NotNil(t, k) + ctx, err := k.Parse([]string{}) + require.NoError(t, err) + require.NotNil(t, ctx) + require.Nil(t, cli.F) +} + +func TestStringPointerAlias(t *testing.T) { + type Foo string + var cli struct { + F *Foo + } + k, err := kong.New(&cli) + require.NoError(t, err) + require.NotNil(t, k) + ctx, err := k.Parse([]string{"--f=value"}) + require.NoError(t, err) + require.NotNil(t, ctx) + require.NotNil(t, cli.F) + require.Equal(t, Foo("value"), *cli.F) +} + +func TestStringPointerEmptyValue(t *testing.T) { + var cli struct { + F *string + G *string + } + k, err := kong.New(&cli) + require.NoError(t, err) + require.NotNil(t, k) + ctx, err := k.Parse([]string{"--f", "", "--g="}) + require.NoError(t, err) + require.NotNil(t, ctx) + require.NotNil(t, cli.F) + require.NotNil(t, cli.G) + require.Equal(t, "", *cli.F) + require.Equal(t, "", *cli.G) +} + +func TestIntPtr(t *testing.T) { + var cli struct { + F *int + G *int + } + k, err := kong.New(&cli) + require.NoError(t, err) + require.NotNil(t, k) + ctx, err := k.Parse([]string{"--f=6"}) + require.NoError(t, err) + require.NotNil(t, ctx) + require.NotNil(t, cli.F) + require.Nil(t, cli.G) + require.Equal(t, 6, *cli.F) +} diff --git a/mapper.go b/mapper.go index 139b2df..b45aae2 100644 --- a/mapper.go +++ b/mapper.go @@ -275,7 +275,8 @@ func (r *Registry) RegisterDefaults() *Registry { RegisterName("path", pathMapper(r)). RegisterName("existingfile", existingFileMapper(r)). RegisterName("existingdir", existingDirMapper(r)). - RegisterName("counter", counterMapper()) + RegisterName("counter", counterMapper()). + RegisterKind(reflect.Ptr, ptrMapper(r)) } type boolMapper struct{} @@ -654,6 +655,22 @@ func existingDirMapper(r *Registry) MapperFunc { } } +func ptrMapper(r *Registry) MapperFunc { + return func(ctx *DecodeContext, target reflect.Value) error { + elem := reflect.New(target.Type().Elem()).Elem() + nestedMapper := r.ForValue(elem) + if nestedMapper == nil { + return errors.New("cannot find member for pointer element type") + } + err := nestedMapper.Decode(ctx, elem) + if err != nil { + return err + } + target.Set(elem.Addr()) + return nil + } +} + func counterMapper() MapperFunc { return func(ctx *DecodeContext, target reflect.Value) error { if ctx.Scan.Peek().Type == FlagValueToken {