github.com/alecthomas/kong@v0.9.1-0.20240410131203-2ab5733f1179/resolver_test.go (about) 1 package kong_test 2 3 import ( 4 "errors" 5 "os" 6 "reflect" 7 "strings" 8 "testing" 9 10 "github.com/alecthomas/assert/v2" 11 "github.com/alecthomas/kong" 12 ) 13 14 type envMap map[string]string 15 16 func tempEnv(env envMap) func() { 17 for k, v := range env { 18 os.Setenv(k, v) 19 } 20 21 return func() { 22 for k := range env { 23 os.Unsetenv(k) 24 } 25 } 26 } 27 28 func newEnvParser(t *testing.T, cli interface{}, env envMap, options ...kong.Option) (*kong.Kong, func()) { 29 t.Helper() 30 restoreEnv := tempEnv(env) 31 parser := mustNew(t, cli, options...) 32 return parser, restoreEnv 33 } 34 35 func TestEnvarsFlagBasic(t *testing.T) { 36 var cli struct { 37 String string `env:"KONG_STRING"` 38 Slice []int `env:"KONG_SLICE"` 39 Interp string `env:"${kongInterp}"` 40 } 41 kongInterpEnv := "KONG_INTERP" 42 parser, unsetEnvs := newEnvParser(t, &cli, 43 envMap{ 44 "KONG_STRING": "bye", 45 "KONG_SLICE": "5,2,9", 46 "KONG_INTERP": "foo", 47 }, 48 kong.Vars{ 49 "kongInterp": kongInterpEnv, 50 }, 51 ) 52 defer unsetEnvs() 53 54 _, err := parser.Parse([]string{}) 55 assert.NoError(t, err) 56 assert.Equal(t, "bye", cli.String) 57 assert.Equal(t, []int{5, 2, 9}, cli.Slice) 58 assert.Equal(t, "foo", cli.Interp) 59 } 60 61 func TestEnvarsFlagMultiple(t *testing.T) { 62 var cli struct { 63 FirstENVPresent string `env:"KONG_TEST1_1,KONG_TEST1_2"` 64 SecondENVPresent string `env:"KONG_TEST2_1,KONG_TEST2_2"` 65 } 66 parser, unsetEnvs := newEnvParser(t, &cli, 67 envMap{ 68 "KONG_TEST1_1": "value1.1", 69 "KONG_TEST1_2": "value1.2", 70 "KONG_TEST2_2": "value2.2", 71 }, 72 ) 73 defer unsetEnvs() 74 75 _, err := parser.Parse([]string{}) 76 assert.NoError(t, err) 77 assert.Equal(t, "value1.1", cli.FirstENVPresent) 78 assert.Equal(t, "value2.2", cli.SecondENVPresent) 79 } 80 81 func TestEnvarsFlagOverride(t *testing.T) { 82 var cli struct { 83 Flag string `env:"KONG_FLAG"` 84 } 85 parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_FLAG": "bye"}) 86 defer restoreEnv() 87 88 _, err := parser.Parse([]string{"--flag=hello"}) 89 assert.NoError(t, err) 90 assert.Equal(t, "hello", cli.Flag) 91 } 92 93 func TestEnvarsTag(t *testing.T) { 94 var cli struct { 95 Slice []int `env:"KONG_NUMBERS"` 96 } 97 parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "5,2,9"}) 98 defer restoreEnv() 99 100 _, err := parser.Parse([]string{}) 101 assert.NoError(t, err) 102 assert.Equal(t, []int{5, 2, 9}, cli.Slice) 103 } 104 105 func TestEnvarsEnvPrefix(t *testing.T) { 106 type Anonymous struct { 107 Slice []int `env:"NUMBERS"` 108 } 109 var cli struct { 110 Anonymous `envprefix:"KONG_"` 111 } 112 parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "1,2,3"}) 113 defer restoreEnv() 114 115 _, err := parser.Parse([]string{}) 116 assert.NoError(t, err) 117 assert.Equal(t, []int{1, 2, 3}, cli.Slice) 118 } 119 120 func TestEnvarsEnvPrefixMultiple(t *testing.T) { 121 type Anonymous struct { 122 Slice1 []int `env:"NUMBERS1_1,NUMBERS1_2"` 123 Slice2 []int `env:"NUMBERS2_1,NUMBERS2_2"` 124 } 125 var cli struct { 126 Anonymous `envprefix:"KONG_"` 127 } 128 parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_NUMBERS1_1": "1,2,3", "KONG_NUMBERS2_2": "5,6,7"}) 129 defer restoreEnv() 130 131 _, err := parser.Parse([]string{}) 132 assert.NoError(t, err) 133 assert.Equal(t, []int{1, 2, 3}, cli.Slice1) 134 assert.Equal(t, []int{5, 6, 7}, cli.Slice2) 135 } 136 137 func TestEnvarsNestedEnvPrefix(t *testing.T) { 138 type NestedAnonymous struct { 139 String string `env:"STRING"` 140 } 141 type Anonymous struct { 142 NestedAnonymous `envprefix:"ANON_"` 143 } 144 var cli struct { 145 Anonymous `envprefix:"KONG_"` 146 } 147 parser, restoreEnv := newEnvParser(t, &cli, envMap{"KONG_ANON_STRING": "abc"}) 148 defer restoreEnv() 149 150 _, err := parser.Parse([]string{}) 151 assert.NoError(t, err) 152 assert.Equal(t, "abc", cli.String) 153 } 154 155 func TestEnvarsWithDefault(t *testing.T) { 156 var cli struct { 157 Flag string `env:"KONG_FLAG" default:"default"` 158 } 159 parser, restoreEnv := newEnvParser(t, &cli, envMap{}) 160 defer restoreEnv() 161 162 _, err := parser.Parse(nil) 163 assert.NoError(t, err) 164 assert.Equal(t, "default", cli.Flag) 165 166 parser, restoreEnv = newEnvParser(t, &cli, envMap{"KONG_FLAG": "moo"}) 167 defer restoreEnv() 168 _, err = parser.Parse(nil) 169 assert.NoError(t, err) 170 assert.Equal(t, "moo", cli.Flag) 171 } 172 173 func TestEnv(t *testing.T) { 174 type Embed struct { 175 Flag string 176 } 177 type Cli struct { 178 One Embed `prefix:"one-" embed:""` 179 Two Embed `prefix:"two." embed:""` 180 Three Embed `prefix:"three_" embed:""` 181 Four Embed `prefix:"four_" embed:""` 182 Five bool 183 Six bool `env:"-"` 184 } 185 186 var cli Cli 187 188 expected := Cli{ 189 One: Embed{Flag: "one"}, 190 Two: Embed{Flag: "two"}, 191 Three: Embed{Flag: "three"}, 192 Four: Embed{Flag: "four"}, 193 Five: true, 194 } 195 196 // With the prefix 197 parser, unsetEnvs := newEnvParser(t, &cli, envMap{ 198 "KONG_ONE_FLAG": "one", 199 "KONG_TWO_FLAG": "two", 200 "KONG_THREE_FLAG": "three", 201 "KONG_FOUR_FLAG": "four", 202 "KONG_FIVE": "true", 203 "KONG_SIX": "true", 204 }, kong.DefaultEnvars("KONG")) 205 defer unsetEnvs() 206 207 _, err := parser.Parse(nil) 208 assert.NoError(t, err) 209 assert.Equal(t, expected, cli) 210 211 // Without the prefix 212 parser, unsetEnvs = newEnvParser(t, &cli, envMap{ 213 "ONE_FLAG": "one", 214 "TWO_FLAG": "two", 215 "THREE_FLAG": "three", 216 "FOUR_FLAG": "four", 217 "FIVE": "true", 218 "SIX": "true", 219 }, kong.DefaultEnvars("")) 220 defer unsetEnvs() 221 222 _, err = parser.Parse(nil) 223 assert.NoError(t, err) 224 assert.Equal(t, expected, cli) 225 } 226 227 func TestJSONBasic(t *testing.T) { 228 type Embed struct { 229 String string 230 } 231 232 var cli struct { 233 String string 234 Slice []int 235 SliceWithCommas []string 236 Bool bool 237 238 One Embed `prefix:"one." embed:""` 239 Two Embed `prefix:"two." embed:""` 240 } 241 242 json := `{ 243 "string": "🍕", 244 "slice": [5, 8], 245 "bool": true, 246 "sliceWithCommas": ["a,b", "c"], 247 "one":{ 248 "string": "one value" 249 }, 250 "two.string": "two value" 251 }` 252 253 r, err := kong.JSON(strings.NewReader(json)) 254 assert.NoError(t, err) 255 256 parser := mustNew(t, &cli, kong.Resolvers(r)) 257 _, err = parser.Parse([]string{}) 258 assert.NoError(t, err) 259 assert.Equal(t, "🍕", cli.String) 260 assert.Equal(t, []int{5, 8}, cli.Slice) 261 assert.Equal(t, []string{"a,b", "c"}, cli.SliceWithCommas) 262 assert.Equal(t, "one value", cli.One.String) 263 assert.Equal(t, "two value", cli.Two.String) 264 assert.True(t, cli.Bool) 265 } 266 267 type testUppercaseMapper struct{} 268 269 func (testUppercaseMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error { 270 var value string 271 err := ctx.Scan.PopValueInto("lowercase", &value) 272 if err != nil { 273 return err 274 } 275 target.SetString(strings.ToUpper(value)) 276 return nil 277 } 278 279 func TestResolversWithMappers(t *testing.T) { 280 var cli struct { 281 Flag string `env:"KONG_MOO" type:"upper"` 282 } 283 284 restoreEnv := tempEnv(envMap{"KONG_MOO": "meow"}) 285 defer restoreEnv() 286 287 parser := mustNew(t, &cli, 288 kong.NamedMapper("upper", testUppercaseMapper{}), 289 ) 290 _, err := parser.Parse([]string{}) 291 assert.NoError(t, err) 292 assert.Equal(t, "MEOW", cli.Flag) 293 } 294 295 func TestResolverWithBool(t *testing.T) { 296 var cli struct { 297 Bool bool 298 } 299 300 var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { 301 if flag.Name == "bool" { 302 return true, nil 303 } 304 return nil, nil 305 } 306 307 p := mustNew(t, &cli, kong.Resolvers(resolver)) 308 309 _, err := p.Parse(nil) 310 assert.NoError(t, err) 311 assert.True(t, cli.Bool) 312 } 313 314 func TestLastResolverWins(t *testing.T) { 315 var cli struct { 316 Int []int 317 } 318 319 var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { 320 if flag.Name == "int" { 321 return 1, nil 322 } 323 return nil, nil 324 } 325 326 var second kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { 327 if flag.Name == "int" { 328 return 2, nil 329 } 330 return nil, nil 331 } 332 333 p := mustNew(t, &cli, kong.Resolvers(first, second)) 334 _, err := p.Parse(nil) 335 assert.NoError(t, err) 336 assert.Equal(t, []int{2}, cli.Int) 337 } 338 339 func TestResolverSatisfiesRequired(t *testing.T) { 340 var cli struct { 341 Int int `required` 342 } 343 var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { 344 if flag.Name == "int" { 345 return 1, nil 346 } 347 return nil, nil 348 } 349 _, err := mustNew(t, &cli, kong.Resolvers(resolver)).Parse(nil) 350 assert.NoError(t, err) 351 assert.Equal(t, 1, cli.Int) 352 } 353 354 func TestResolverTriggersHooks(t *testing.T) { 355 ctx := &hookContext{} 356 357 var cli struct { 358 Flag hookValue 359 } 360 361 var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { 362 if flag.Name == "flag" { 363 return "one", nil 364 } 365 return nil, nil 366 } 367 368 _, err := mustNew(t, &cli, kong.Bind(ctx), kong.Resolvers(first)).Parse(nil) 369 assert.NoError(t, err) 370 371 assert.Equal(t, "one", string(cli.Flag)) 372 assert.Equal(t, []string{"before:", "after:one"}, ctx.values) 373 } 374 375 type validatingResolver struct { 376 err error 377 } 378 379 func (v *validatingResolver) Validate(app *kong.Application) error { return v.err } 380 func (v *validatingResolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (interface{}, error) { 381 return nil, nil 382 } 383 384 func TestValidatingResolverErrors(t *testing.T) { 385 resolver := &validatingResolver{err: errors.New("invalid")} 386 var cli struct{} 387 _, err := mustNew(t, &cli, kong.Resolvers(resolver)).Parse(nil) 388 assert.EqualError(t, err, "invalid") 389 }