Codebase list golang-github-alecthomas-kong / HEAD resolver_test.go
HEAD

Tree @HEAD (Download .tar.gz)

resolver_test.go @HEADraw · history · blame

package kong_test

import (
	"errors"
	"os"
	"reflect"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/alecthomas/kong"
)

type envMap map[string]string

func tempEnv(env envMap) func() {
	for k, v := range env {
		os.Setenv(k, v)
	}

	return func() {
		for k := range env {
			os.Unsetenv(k)
		}
	}
}

func newEnvParser(t *testing.T, cli interface{}, env envMap, options ...kong.Option) (*kong.Kong, func()) {
	t.Helper()
	restoreEnv := tempEnv(env)
	parser := mustNew(t, cli, options...)
	return parser, restoreEnv
}

func TestEnvarsFlagBasic(t *testing.T) {
	var cli struct {
		String string `env:"KONG_STRING"`
		Slice  []int  `env:"KONG_SLICE"`
		Interp string `env:"${kongInterp}"`
	}
	kongInterpEnv := "KONG_INTERP"
	parser, unsetEnvs := newEnvParser(t, &cli,
		envMap{
			"KONG_STRING": "bye",
			"KONG_SLICE":  "5,2,9",
			"KONG_INTERP": "foo",
		},
		kong.Vars{
			"kongInterp": kongInterpEnv,
		},
	)
	defer unsetEnvs()

	_, err := parser.Parse([]string{})
	require.NoError(t, err)
	require.Equal(t, "bye", cli.String)
	require.Equal(t, []int{5, 2, 9}, cli.Slice)
	require.Equal(t, "foo", cli.Interp)
}

func TestEnvarsFlagOverride(t *testing.T) {
	var cli struct {
		Flag string `env:"KONG_FLAG"`
	}
	parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_FLAG": "bye"})
	defer restoreEnv()

	_, err := parser.Parse([]string{"--flag=hello"})
	require.NoError(t, err)
	require.Equal(t, "hello", cli.Flag)
}

func TestEnvarsTag(t *testing.T) {
	var cli struct {
		Slice []int `env:"KONG_NUMBERS"`
	}
	parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "5,2,9"})
	defer restoreEnv()

	_, err := parser.Parse([]string{})
	require.NoError(t, err)
	require.Equal(t, []int{5, 2, 9}, cli.Slice)
}

func TestEnvarsEnvPrefix(t *testing.T) {
	type Anonymous struct {
		Slice []int `env:"NUMBERS"`
	}
	var cli struct {
		Anonymous `envprefix:"KONG_"`
	}
	parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "1,2,3"})
	defer restoreEnv()

	_, err := parser.Parse([]string{})
	require.NoError(t, err)
	require.Equal(t, []int{1, 2, 3}, cli.Slice)
}

func TestEnvarsNestedEnvPrefix(t *testing.T) {
	type NestedAnonymous struct {
		String string `env:"STRING"`
	}
	type Anonymous struct {
		NestedAnonymous `envprefix:"ANON_"`
	}
	var cli struct {
		Anonymous `envprefix:"KONG_"`
	}
	parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_ANON_STRING": "abc"})
	defer restoreEnv()

	_, err := parser.Parse([]string{})
	require.NoError(t, err)
	require.Equal(t, "abc", cli.String)
}

func TestEnvarsWithDefault(t *testing.T) {
	var cli struct {
		Flag string `env:"KONG_FLAG" default:"default"`
	}
	parser, restoreEnv := newEnvParser(t, &cli, envMap{})
	defer restoreEnv()

	_, err := parser.Parse(nil)
	require.NoError(t, err)
	require.Equal(t, "default", cli.Flag)

	parser, restoreEnv = newEnvParser(t, &cli, envMap{"KONG_FLAG": "moo"})
	defer restoreEnv()
	_, err = parser.Parse(nil)
	require.NoError(t, err)
	require.Equal(t, "moo", cli.Flag)
}

func TestEnv(t *testing.T) {
	type Embed struct {
		Flag string
	}
	type Cli struct {
		One   Embed `prefix:"one-" embed:""`
		Two   Embed `prefix:"two." embed:""`
		Three Embed `prefix:"three_" embed:""`
		Four  Embed `prefix:"four_" embed:""`
		Five  bool
		Six   bool `env:"-"`
	}

	var cli Cli

	expected := Cli{
		One:   Embed{Flag: "one"},
		Two:   Embed{Flag: "two"},
		Three: Embed{Flag: "three"},
		Four:  Embed{Flag: "four"},
		Five:  true,
	}

	// With the prefix
	parser, unsetEnvs := newEnvParser(t, &cli, envMap{
		"KONG_ONE_FLAG":   "one",
		"KONG_TWO_FLAG":   "two",
		"KONG_THREE_FLAG": "three",
		"KONG_FOUR_FLAG":  "four",
		"KONG_FIVE":       "true",
		"KONG_SIX":        "true",
	}, kong.DefaultEnvars("KONG"))
	defer unsetEnvs()

	_, err := parser.Parse(nil)
	require.NoError(t, err)
	require.Equal(t, expected, cli)

	// Without the prefix
	parser, unsetEnvs = newEnvParser(t, &cli, envMap{
		"ONE_FLAG":   "one",
		"TWO_FLAG":   "two",
		"THREE_FLAG": "three",
		"FOUR_FLAG":  "four",
		"FIVE":       "true",
		"SIX":        "true",
	}, kong.DefaultEnvars(""))
	defer unsetEnvs()

	_, err = parser.Parse(nil)
	require.NoError(t, err)
	require.Equal(t, expected, cli)
}

func TestJSONBasic(t *testing.T) {
	type Embed struct {
		String string
	}

	var cli struct {
		String          string
		Slice           []int
		SliceWithCommas []string
		Bool            bool

		One Embed `prefix:"one." embed:""`
		Two Embed `prefix:"two." embed:""`
	}

	json := `{
		"string": "🍕",
		"slice": [5, 8],
		"bool": true,
		"slice_with_commas": ["a,b", "c"],
		"one":{
			"string": "one value"
		},
		"two.string": "two value"
	}`

	r, err := kong.JSON(strings.NewReader(json))
	require.NoError(t, err)

	parser := mustNew(t, &cli, kong.Resolvers(r))
	_, err = parser.Parse([]string{})
	require.NoError(t, err)
	require.Equal(t, "🍕", cli.String)
	require.Equal(t, []int{5, 8}, cli.Slice)
	require.Equal(t, []string{"a,b", "c"}, cli.SliceWithCommas)
	require.Equal(t, "one value", cli.One.String)
	require.Equal(t, "two value", cli.Two.String)
	require.True(t, cli.Bool)
}

type testUppercaseMapper struct{}

func (testUppercaseMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error {
	var value string
	err := ctx.Scan.PopValueInto("lowercase", &value)
	if err != nil {
		return err
	}
	target.SetString(strings.ToUpper(value))
	return nil
}

func TestResolversWithMappers(t *testing.T) {
	var cli struct {
		Flag string `env:"KONG_MOO" type:"upper"`
	}

	restoreEnv := tempEnv(envMap{"KONG_MOO": "meow"})
	defer restoreEnv()

	parser := mustNew(t, &cli,
		kong.NamedMapper("upper", testUppercaseMapper{}),
	)
	_, err := parser.Parse([]string{})
	require.NoError(t, err)
	require.Equal(t, "MEOW", cli.Flag)
}

func TestResolverWithBool(t *testing.T) {
	var cli struct {
		Bool bool
	}

	var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
		if flag.Name == "bool" {
			return true, nil
		}
		return nil, nil
	}

	p := mustNew(t, &cli, kong.Resolvers(resolver))

	_, err := p.Parse(nil)
	require.NoError(t, err)
	require.True(t, cli.Bool)
}

func TestLastResolverWins(t *testing.T) {
	var cli struct {
		Int []int
	}

	var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
		if flag.Name == "int" {
			return 1, nil
		}
		return nil, nil
	}

	var second kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
		if flag.Name == "int" {
			return 2, nil
		}
		return nil, nil
	}

	p := mustNew(t, &cli, kong.Resolvers(first, second))
	_, err := p.Parse(nil)
	require.NoError(t, err)
	require.Equal(t, []int{2}, cli.Int)
}

func TestResolverSatisfiesRequired(t *testing.T) {
	var cli struct {
		Int int `required`
	}
	var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
		if flag.Name == "int" {
			return 1, nil
		}
		return nil, nil
	}
	_, err := mustNew(t, &cli, kong.Resolvers(resolver)).Parse(nil)
	require.NoError(t, err)
	require.Equal(t, 1, cli.Int)
}

func TestResolverTriggersHooks(t *testing.T) {
	ctx := &hookContext{}

	var cli struct {
		Flag hookValue
	}

	var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
		if flag.Name == "flag" {
			return "one", nil
		}
		return nil, nil
	}

	_, err := mustNew(t, &cli, kong.Bind(ctx), kong.Resolvers(first)).Parse(nil)
	require.NoError(t, err)

	require.Equal(t, "one", string(cli.Flag))
	require.Equal(t, []string{"before:", "after:one"}, ctx.values)
}

type validatingResolver struct {
	err error
}

func (v *validatingResolver) Validate(app *kong.Application) error { return v.err }
func (v *validatingResolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) {
	return nil, nil
}

func TestValidatingResolverErrors(t *testing.T) {
	resolver := &validatingResolver{err: errors.New("invalid")}
	var cli struct{}
	_, err := mustNew(t, &cli, kong.Resolvers(resolver)).Parse(nil)
	require.EqualError(t, err, "invalid")
}