diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6a6e8b5..f98f5b7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,11 +10,13 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go: [1.18.x, 1.19.x] + go: [1.18.x, 1.19.x, 1.20.x] runs-on: ${{ matrix.os }} steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} - uses: actions/checkout@v3 - - run: go test -v ./... + with: + fetch-depth: 0 + - run: make diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1ee176 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.coverprofile +*.exe +*.orig +.*envrc +.envrc +.idea +/.local/ +/site/ +coverage.txt +vendor diff --git a/.testdata/alt-config.json b/.testdata/alt-config.json new file mode 100644 index 0000000..8c5a9f5 --- /dev/null +++ b/.testdata/alt-config.json @@ -0,0 +1,6 @@ +{ + "greet": { + "name": "Berry", + "enthusiasm": "eleven" + } +} diff --git a/.testdata/alt-config.toml b/.testdata/alt-config.toml new file mode 100644 index 0000000..a7d960e --- /dev/null +++ b/.testdata/alt-config.toml @@ -0,0 +1,3 @@ +[greet] +name = "Berry" +enthusiasm = "eleven" diff --git a/.testdata/alt-config.yaml b/.testdata/alt-config.yaml new file mode 100644 index 0000000..e8397f0 --- /dev/null +++ b/.testdata/alt-config.yaml @@ -0,0 +1,3 @@ +greet: + name: Berry + enthusiasm: eleven diff --git a/.testdata/config.json b/.testdata/config.json new file mode 100644 index 0000000..136d0ce --- /dev/null +++ b/.testdata/config.json @@ -0,0 +1,5 @@ +{ + "greet": { + "enthusiasm": 9001 + } +} diff --git a/.testdata/config.toml b/.testdata/config.toml new file mode 100644 index 0000000..b108ee9 --- /dev/null +++ b/.testdata/config.toml @@ -0,0 +1,2 @@ +[greet] +enthusiasm = 9001 diff --git a/.testdata/config.yaml b/.testdata/config.yaml new file mode 100644 index 0000000..c9fbc77 --- /dev/null +++ b/.testdata/config.yaml @@ -0,0 +1,2 @@ +greet: + enthusiasm: 9001 diff --git a/.testdata/test_alt_config.json b/.testdata/test_alt_config.json new file mode 100644 index 0000000..6e6dd29 --- /dev/null +++ b/.testdata/test_alt_config.json @@ -0,0 +1,8 @@ +{ + "water_fountain": { + "water": true + }, + "phone_booth": { + "phone": false + } +} diff --git a/.testdata/test_alt_config.toml b/.testdata/test_alt_config.toml new file mode 100644 index 0000000..0896b2a --- /dev/null +++ b/.testdata/test_alt_config.toml @@ -0,0 +1,5 @@ +[water_fountain] +water = true + +[phone_booth] +phone = false diff --git a/.testdata/test_alt_config.yaml b/.testdata/test_alt_config.yaml new file mode 100644 index 0000000..7509096 --- /dev/null +++ b/.testdata/test_alt_config.yaml @@ -0,0 +1,4 @@ +water_fountain: + water: true +phone_booth: + phone: false diff --git a/.testdata/test_config.json b/.testdata/test_config.json new file mode 100644 index 0000000..74c5345 --- /dev/null +++ b/.testdata/test_config.json @@ -0,0 +1,8 @@ +{ + "water_fountain": { + "water": false + }, + "woodstock": { + "wood": false + } +} diff --git a/.testdata/test_config.toml b/.testdata/test_config.toml new file mode 100644 index 0000000..3585c37 --- /dev/null +++ b/.testdata/test_config.toml @@ -0,0 +1,5 @@ +[water_fountain] +water = false + +[woodstock] +wood = false diff --git a/.testdata/test_config.yaml b/.testdata/test_config.yaml new file mode 100644 index 0000000..59d485c --- /dev/null +++ b/.testdata/test_config.yaml @@ -0,0 +1,4 @@ +water_fountain: + water: false +woodstock: + wood: false diff --git a/LICENSE b/LICENSE index 5f463d5..7e65e54 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 urfave contributors +Copyright (c) 2023 urfave contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..753d6d1 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +.PHONY: all +all: vet test show-cover + +.PHONY: vet +vet: + go vet -v ./... + +.PHONY: test +test: + go test -v -cover -coverprofile=coverage.txt ./... + +.PHONY: show-cover +show-cover: + go tool cover -func=coverage.txt diff --git a/README.md b/README.md index fd2840e..8552700 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ -# cli-altsrc +# Welcome to urfave/cli-altsrc/v3 -Configuration source integration library for urfave/cli +[![Run Tests](https://github.com/urfave/cli-altsrc/actions/workflows/main.yml/badge.svg)](https://github.com/urfave/cli-altsrc/actions/workflows/main.yml) +[![Go Reference](https://pkg.go.dev/badge/github.com/urfave/cli-altsrc/v3.svg)](https://pkg.go.dev/github.com/urfave/cli-altsrc/v3) +[![Go Report Card](https://goreportcard.com/badge/github.com/urfave/cli-altsrc/v3)](https://goreportcard.com/report/github.com/urfave/cli-altsrc/v3) + +urfave/cli-altsrc/v3 is an extended value source integration library for [urfave/cli/v3] with support for JSON, +YAML, and TOML. The primary reason for this to be a separate library is that third-party libraries are used for these +features which are otherwise not used throughout [urfave/cli/v3]. + +[urfave/cli/v3]: github.com/urfave/cli diff --git a/altsrc.go b/altsrc.go new file mode 100644 index 0000000..321902d --- /dev/null +++ b/altsrc.go @@ -0,0 +1,106 @@ +package altsrc + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "runtime" + "strings" +) + +var ( + Err = errors.New("urfave/cli-altsrc error") + + isTracingOn = os.Getenv("URFAVE_CLI_TRACING") == "on" +) + +func tracef(format string, a ...any) { + if !isTracingOn { + return + } + + if !strings.HasSuffix(format, "\n") { + format = format + "\n" + } + + pc, file, line, _ := runtime.Caller(1) + cf := runtime.FuncForPC(pc) + + fmt.Fprintf( + os.Stderr, + strings.Join([]string{ + "## URFAVE CLI TRACE ", + file, + ":", + fmt.Sprintf("%v", line), + " ", + fmt.Sprintf("(%s)", cf.Name()), + " ", + format, + }, ""), + a..., + ) +} + +func readURI(uriString string) ([]byte, error) { + u, err := url.Parse(uriString) + if err != nil { + return nil, err + } + + if u.Host != "" { // i have a host, now do i support the scheme? + switch u.Scheme { + case "http", "https": + res, err := http.Get(uriString) + if err != nil { + return nil, err + } + return io.ReadAll(res.Body) + default: + return nil, fmt.Errorf("%[1]w: scheme of %[2]q is unsupported", Err, uriString) + } + } else if u.Path != "" || + (runtime.GOOS == "windows" && strings.Contains(u.String(), "\\")) { + if _, notFoundFileErr := os.Stat(uriString); notFoundFileErr != nil { + return nil, fmt.Errorf("%[1]w: cannot read from %[2]q because it does not exist", Err, uriString) + } + return os.ReadFile(uriString) + } + + return nil, fmt.Errorf("%[1]w: unable to determine how to load from %[2]q", Err, uriString) +} + +// nestedVal checks if the name has '.' delimiters. +// If so, it tries to traverse the tree by the '.' delimited sections to find +// a nested value for the key. +func nestedVal(name string, tree map[any]any) (any, bool) { + if sections := strings.Split(name, "."); len(sections) > 1 { + node := tree + for _, section := range sections[:len(sections)-1] { + child, ok := node[section] + if !ok { + return nil, false + } + + switch child := child.(type) { + case map[string]any: + node = make(map[any]any, len(child)) + for k, v := range child { + node[k] = v + } + case map[any]any: + node = child + default: + return nil, false + } + } + if val, ok := node[sections[len(sections)-1]]; ok { + return val, true + } + } + + return nil, false +} diff --git a/altsrc_test.go b/altsrc_test.go new file mode 100644 index 0000000..812a8ee --- /dev/null +++ b/altsrc_test.go @@ -0,0 +1,17 @@ +package altsrc + +import ( + "context" + "time" + + "github.com/urfave/cli-altsrc/v3/internal" +) + +var ( + testdataDir = func() string { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + return internal.MustTestdataDir(ctx) + }() +) diff --git a/default_input_source.go b/default_input_source.go deleted file mode 100644 index 7fda719..0000000 --- a/default_input_source.go +++ /dev/null @@ -1,6 +0,0 @@ -package altsrc - -// defaultInputSource creates a default InputSourceContext. -func defaultInputSource() (InputSourceContext, error) { - return &MapInputSource{file: "", valueMap: map[interface{}]interface{}{}}, nil -} diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..dc6e0c2 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,151 @@ +package altsrc_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + "time" + + altsrc "github.com/urfave/cli-altsrc/v3" + "github.com/urfave/cli-altsrc/v3/internal" + "github.com/urfave/cli/v3" +) + +var ( + testdataDir = func() string { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + return internal.MustTestdataDir(ctx) + }() +) + +func ExampleYAML() { + configFiles := []string{ + filepath.Join(testdataDir, "config.yaml"), + filepath.Join(testdataDir, "alt-config.yaml"), + } + + app := &cli.Command{ + Name: "greet", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Sources: altsrc.YAML("greet.name", configFiles...), + }, + &cli.IntFlag{ + Name: "enthusiasm", + Aliases: []string{"!"}, + Sources: altsrc.YAML("greet.enthusiasm", configFiles...), + }, + }, + Action: func(cCtx *cli.Context) error { + punct := "" + if cCtx.Int("enthusiasm") > 9000 { + punct = "!" + } + + fmt.Fprintf(os.Stdout, "Hello, %[1]v%[2]v\n", cCtx.String("name"), punct) + + return nil + }, + } + + // Simulating os.Args + os.Args = []string{"greet"} + + if err := app.Run(context.Background(), os.Args); err != nil { + fmt.Fprintf(os.Stdout, "OH NO: %[1]v\n", err) + } + + // Output: + // Hello, Berry! +} + +func ExampleJSON() { + configFiles := []string{ + filepath.Join(testdataDir, "config.json"), + filepath.Join(testdataDir, "alt-config.json"), + } + + app := &cli.Command{ + Name: "greet", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Sources: altsrc.JSON("greet.name", configFiles...), + }, + &cli.IntFlag{ + Name: "enthusiasm", + Aliases: []string{"!"}, + Sources: altsrc.JSON("greet.enthusiasm", configFiles...), + }, + }, + Action: func(cCtx *cli.Context) error { + punct := "" + if cCtx.Int("enthusiasm") > 9000 { + punct = "!" + } + + fmt.Fprintf(os.Stdout, "Hello, %[1]v%[2]v\n", cCtx.String("name"), punct) + + return nil + }, + } + + // Simulating os.Args + os.Args = []string{"greet"} + + if err := app.Run(context.Background(), os.Args); err != nil { + fmt.Fprintf(os.Stdout, "OH NO: %[1]v\n", err) + } + + // Output: + // Hello, Berry! +} + +func ExampleTOML() { + configFiles := []string{ + filepath.Join(testdataDir, "config.toml"), + filepath.Join(testdataDir, "alt-config.toml"), + } + + app := &cli.Command{ + Name: "greet", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Sources: altsrc.TOML("greet.name", configFiles...), + }, + &cli.IntFlag{ + Name: "enthusiasm", + Aliases: []string{"!"}, + Sources: altsrc.TOML("greet.enthusiasm", configFiles...), + }, + }, + Action: func(cCtx *cli.Context) error { + punct := "" + if cCtx.Int("enthusiasm") > 9000 { + punct = "!" + } + + fmt.Fprintf(os.Stdout, "Hello, %[1]v%[2]v\n", cCtx.String("name"), punct) + + return nil + }, + } + + // Simulating os.Args + os.Args = []string{"greet"} + + if err := app.Run(context.Background(), os.Args); err != nil { + fmt.Fprintf(os.Stdout, "OH NO: %[1]v\n", err) + } + + // Output: + // Hello, Berry! +} diff --git a/file_source_cache.go b/file_source_cache.go new file mode 100644 index 0000000..eeacb8f --- /dev/null +++ b/file_source_cache.go @@ -0,0 +1,30 @@ +package altsrc + +type fileSourceCache[T any] struct { + file string + m *T + f func(string, any) error +} + +func (fsc *fileSourceCache[T]) Get() T { + if fsc.m == nil { + res := new(T) + if err := fsc.f(fsc.file, res); err == nil { + fsc.m = res + } else { + tracef("failed to unmarshal from file %[1]q: %[2]v", fsc.file, err) + } + } + + if fsc.m == nil { + tracef("returning empty") + + return *(new(T)) + } + + return *fsc.m +} + +type mapAnyAnyFileSourceCache = fileSourceCache[map[any]any] + +type tomlMapFileSourceCache = fileSourceCache[tomlMap] diff --git a/flag-spec.yaml b/flag-spec.yaml deleted file mode 100644 index 8c7db6f..0000000 --- a/flag-spec.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# NOTE: this file is used by the tool defined in -# ./cmd/urfave-cli-genflags/main.go which uses the -# `Spec` type that maps to this file structure. -flag_types: - Bool: - Duration: - Float64: - Generic: - Int64: - Int: - IntSlice: - Int64Slice: - Float64Slice: - String: - Path: - StringSlice: - Uint64: - Uint: \ No newline at end of file diff --git a/flag.go b/flag.go deleted file mode 100644 index 50ebd9e..0000000 --- a/flag.go +++ /dev/null @@ -1,328 +0,0 @@ -package altsrc - -import ( - "fmt" - "path/filepath" - "strconv" - "syscall" - - "github.com/urfave/cli/v3" -) - -// FlagInputSourceExtension is an extension interface of cli.Flag that -// allows a value to be set on the existing parsed flags. -type FlagInputSourceExtension interface { - cli.Flag - ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error -} - -// ApplyInputSourceValues iterates over all provided flags and -// executes ApplyInputSourceValue on flags implementing the -// FlagInputSourceExtension interface to initialize these flags -// to an alternate input source. -func ApplyInputSourceValues(cCtx *cli.Context, inputSourceContext InputSourceContext, flags []cli.Flag) error { - for _, f := range flags { - inputSourceExtendedFlag, isType := f.(FlagInputSourceExtension) - if isType { - err := inputSourceExtendedFlag.ApplyInputSourceValue(cCtx, inputSourceContext) - if err != nil { - return err - } - } - } - - return nil -} - -// InitInputSource is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new -// input source based on the func provided. If there is no error it will then apply the new input source to any flags -// that are supported by the input source -func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceContext, error)) cli.BeforeFunc { - return func(cCtx *cli.Context) error { - inputSource, err := createInputSource() - if err != nil { - return fmt.Errorf("Unable to create input source: inner error: \n'%v'", err.Error()) - } - - return ApplyInputSourceValues(cCtx, inputSource, flags) - } -} - -// InitInputSourceWithContext is used to to setup an InputSourceContext on a cli.Command Before method. It will create a new -// input source based on the func provided with potentially using existing cli.Context values to initialize itself. If there is -// no error it will then apply the new input source to any flags that are supported by the input source -func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(cCtx *cli.Context) (InputSourceContext, error)) cli.BeforeFunc { - return func(cCtx *cli.Context) error { - inputSource, err := createInputSource(cCtx) - if err != nil { - return fmt.Errorf("Unable to create input source with context: inner error: \n'%v'", err.Error()) - } - - return ApplyInputSourceValues(cCtx, inputSource, flags) - } -} - -// ApplyInputSourceValue applies a generic value to the flagSet if required -func (f *GenericFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.GenericFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Generic(name) - if err != nil { - return err - } - if value == nil { - continue - } - for _, n := range f.Names() { - _ = f.set.Set(n, value.String()) - } - } - - return nil -} - -// ApplyInputSourceValue applies a StringSlice value to the flagSet if required -func (f *StringSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.StringSliceFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.StringSlice(name) - if err != nil { - return err - } - if value == nil { - continue - } - var sliceValue = *(cli.NewStringSlice(value...)) - for _, n := range f.Names() { - underlyingFlag := f.set.Lookup(n) - if underlyingFlag == nil { - continue - } - underlyingFlag.Value = &sliceValue - } - if f.Destination != nil { - f.Destination.Set(sliceValue.Serialize()) - } - } - return nil -} - -// ApplyInputSourceValue applies a IntSlice value if required -func (f *IntSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.IntSliceFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.IntSlice(name) - if err != nil { - return err - } - if value == nil { - continue - } - var sliceValue = *(cli.NewIntSlice(value...)) - for _, n := range f.Names() { - underlyingFlag := f.set.Lookup(n) - if underlyingFlag == nil { - continue - } - underlyingFlag.Value = &sliceValue - } - if f.Destination != nil { - f.Destination.Set(sliceValue.Serialize()) - } - } - return nil -} - -// ApplyInputSourceValue applies a Int64Slice value if required -func (f *Int64SliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.Int64SliceFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Int64Slice(name) - if err != nil { - return err - } - if value == nil { - continue - } - var sliceValue = *(cli.NewInt64Slice(value...)) - for _, n := range f.Names() { - underlyingFlag := f.set.Lookup(n) - if underlyingFlag == nil { - continue - } - underlyingFlag.Value = &sliceValue - } - if f.Destination != nil { - f.Destination.Set(sliceValue.Serialize()) - } - } - return nil -} - -// ApplyInputSourceValue applies a Bool value to the flagSet if required -func (f *BoolFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.BoolFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Bool(name) - if err != nil { - return err - } - for _, n := range f.Names() { - _ = f.set.Set(n, strconv.FormatBool(value)) - } - } - return nil -} - -// ApplyInputSourceValue applies a String value to the flagSet if required -func (f *StringFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.StringFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.String(name) - if err != nil { - return err - } - for _, n := range f.Names() { - _ = f.set.Set(n, value) - } - } - return nil -} - -// ApplyInputSourceValue applies a Path value to the flagSet if required -func (f *PathFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.PathFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.String(name) - if err != nil { - return err - } - if value == "" { - continue - } - for _, n := range f.Names() { - if !filepath.IsAbs(value) && isc.Source() != "" { - basePathAbs, err := filepath.Abs(isc.Source()) - if err != nil { - return err - } - value = filepath.Join(filepath.Dir(basePathAbs), value) - } - _ = f.set.Set(n, value) - } - } - return nil -} - -// ApplyInputSourceValue applies a int value to the flagSet if required -func (f *IntFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.IntFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Int(name) - if err != nil { - return err - } - for _, n := range f.Names() { - _ = f.set.Set(n, strconv.FormatInt(int64(value), 10)) - } - } - return nil -} - -// ApplyInputSourceValue applies a Duration value to the flagSet if required -func (f *DurationFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.DurationFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Duration(name) - if err != nil { - return err - } - for _, n := range f.Names() { - _ = f.set.Set(n, value.String()) - } - } - return nil -} - -// ApplyInputSourceValue applies a Float64 value to the flagSet if required -func (f *Float64Flag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.Float64Flag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Float64(name) - if err != nil { - return err - } - floatStr := float64ToString(value) - for _, n := range f.Names() { - _ = f.set.Set(n, floatStr) - } - } - return nil -} - -func isEnvVarSet(envVars []string) bool { - for _, envVar := range envVars { - if _, ok := syscall.Getenv(envVar); ok { - // TODO: Can't use this for bools as - // set means that it was true or false based on - // Bool flag type, should work for other types - return true - } - } - - return false -} - -func float64ToString(f float64) string { - return fmt.Sprintf("%v", f) -} diff --git a/flag_generated.go b/flag_generated.go deleted file mode 100644 index 2a521d1..0000000 --- a/flag_generated.go +++ /dev/null @@ -1,277 +0,0 @@ -// WARNING: this file is generated. DO NOT EDIT - -package altsrc - -import ( - "flag" - - "github.com/urfave/cli/v3" -) - -// BoolFlag is the flag type that wraps cli.BoolFlag to allow -// for other values to be specified -type BoolFlag struct { - *cli.BoolFlag - set *flag.FlagSet -} - -// NewBoolFlag creates a new BoolFlag -func NewBoolFlag(fl *cli.BoolFlag) *BoolFlag { - return &BoolFlag{BoolFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped BoolFlag.Apply -func (f *BoolFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.BoolFlag.Apply(set) -} - -// DurationFlag is the flag type that wraps cli.DurationFlag to allow -// for other values to be specified -type DurationFlag struct { - *cli.DurationFlag - set *flag.FlagSet -} - -// NewDurationFlag creates a new DurationFlag -func NewDurationFlag(fl *cli.DurationFlag) *DurationFlag { - return &DurationFlag{DurationFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped DurationFlag.Apply -func (f *DurationFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.DurationFlag.Apply(set) -} - -// Float64Flag is the flag type that wraps cli.Float64Flag to allow -// for other values to be specified -type Float64Flag struct { - *cli.Float64Flag - set *flag.FlagSet -} - -// NewFloat64Flag creates a new Float64Flag -func NewFloat64Flag(fl *cli.Float64Flag) *Float64Flag { - return &Float64Flag{Float64Flag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped Float64Flag.Apply -func (f *Float64Flag) Apply(set *flag.FlagSet) error { - f.set = set - return f.Float64Flag.Apply(set) -} - -// Float64SliceFlag is the flag type that wraps cli.Float64SliceFlag to allow -// for other values to be specified -type Float64SliceFlag struct { - *cli.Float64SliceFlag - set *flag.FlagSet -} - -// NewFloat64SliceFlag creates a new Float64SliceFlag -func NewFloat64SliceFlag(fl *cli.Float64SliceFlag) *Float64SliceFlag { - return &Float64SliceFlag{Float64SliceFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped Float64SliceFlag.Apply -func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.Float64SliceFlag.Apply(set) -} - -// GenericFlag is the flag type that wraps cli.GenericFlag to allow -// for other values to be specified -type GenericFlag struct { - *cli.GenericFlag - set *flag.FlagSet -} - -// NewGenericFlag creates a new GenericFlag -func NewGenericFlag(fl *cli.GenericFlag) *GenericFlag { - return &GenericFlag{GenericFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped GenericFlag.Apply -func (f *GenericFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.GenericFlag.Apply(set) -} - -// IntFlag is the flag type that wraps cli.IntFlag to allow -// for other values to be specified -type IntFlag struct { - *cli.IntFlag - set *flag.FlagSet -} - -// NewIntFlag creates a new IntFlag -func NewIntFlag(fl *cli.IntFlag) *IntFlag { - return &IntFlag{IntFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped IntFlag.Apply -func (f *IntFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.IntFlag.Apply(set) -} - -// Int64Flag is the flag type that wraps cli.Int64Flag to allow -// for other values to be specified -type Int64Flag struct { - *cli.Int64Flag - set *flag.FlagSet -} - -// NewInt64Flag creates a new Int64Flag -func NewInt64Flag(fl *cli.Int64Flag) *Int64Flag { - return &Int64Flag{Int64Flag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped Int64Flag.Apply -func (f *Int64Flag) Apply(set *flag.FlagSet) error { - f.set = set - return f.Int64Flag.Apply(set) -} - -// Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow -// for other values to be specified -type Int64SliceFlag struct { - *cli.Int64SliceFlag - set *flag.FlagSet -} - -// NewInt64SliceFlag creates a new Int64SliceFlag -func NewInt64SliceFlag(fl *cli.Int64SliceFlag) *Int64SliceFlag { - return &Int64SliceFlag{Int64SliceFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped Int64SliceFlag.Apply -func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.Int64SliceFlag.Apply(set) -} - -// IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow -// for other values to be specified -type IntSliceFlag struct { - *cli.IntSliceFlag - set *flag.FlagSet -} - -// NewIntSliceFlag creates a new IntSliceFlag -func NewIntSliceFlag(fl *cli.IntSliceFlag) *IntSliceFlag { - return &IntSliceFlag{IntSliceFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped IntSliceFlag.Apply -func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.IntSliceFlag.Apply(set) -} - -// PathFlag is the flag type that wraps cli.PathFlag to allow -// for other values to be specified -type PathFlag struct { - *cli.PathFlag - set *flag.FlagSet -} - -// NewPathFlag creates a new PathFlag -func NewPathFlag(fl *cli.PathFlag) *PathFlag { - return &PathFlag{PathFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped PathFlag.Apply -func (f *PathFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.PathFlag.Apply(set) -} - -// StringFlag is the flag type that wraps cli.StringFlag to allow -// for other values to be specified -type StringFlag struct { - *cli.StringFlag - set *flag.FlagSet -} - -// NewStringFlag creates a new StringFlag -func NewStringFlag(fl *cli.StringFlag) *StringFlag { - return &StringFlag{StringFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped StringFlag.Apply -func (f *StringFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.StringFlag.Apply(set) -} - -// StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow -// for other values to be specified -type StringSliceFlag struct { - *cli.StringSliceFlag - set *flag.FlagSet -} - -// NewStringSliceFlag creates a new StringSliceFlag -func NewStringSliceFlag(fl *cli.StringSliceFlag) *StringSliceFlag { - return &StringSliceFlag{StringSliceFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped StringSliceFlag.Apply -func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.StringSliceFlag.Apply(set) -} - -// UintFlag is the flag type that wraps cli.UintFlag to allow -// for other values to be specified -type UintFlag struct { - *cli.UintFlag - set *flag.FlagSet -} - -// NewUintFlag creates a new UintFlag -func NewUintFlag(fl *cli.UintFlag) *UintFlag { - return &UintFlag{UintFlag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped UintFlag.Apply -func (f *UintFlag) Apply(set *flag.FlagSet) error { - f.set = set - return f.UintFlag.Apply(set) -} - -// Uint64Flag is the flag type that wraps cli.Uint64Flag to allow -// for other values to be specified -type Uint64Flag struct { - *cli.Uint64Flag - set *flag.FlagSet -} - -// NewUint64Flag creates a new Uint64Flag -func NewUint64Flag(fl *cli.Uint64Flag) *Uint64Flag { - return &Uint64Flag{Uint64Flag: fl, set: nil} -} - -// Apply saves the flagSet for later usage calls, then calls -// the wrapped Uint64Flag.Apply -func (f *Uint64Flag) Apply(set *flag.FlagSet) error { - f.set = set - return f.Uint64Flag.Apply(set) -} - -// vim:ro diff --git a/flag_test.go b/flag_test.go deleted file mode 100644 index 7f85d74..0000000 --- a/flag_test.go +++ /dev/null @@ -1,743 +0,0 @@ -package altsrc - -import ( - "flag" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/urfave/cli/v3" -) - -type testApplyInputSource struct { - Flag FlagInputSourceExtension - FlagName string - FlagSetName string - Expected string - ContextValueString string - ContextValue flag.Value - EnvVarValue string - EnvVarName string - SourcePath string - MapValue interface{} -} - -type racyInputSource struct { - *MapInputSource -} - -func (ris *racyInputSource) isSet(name string) bool { - if _, ok := ris.MapInputSource.valueMap[name]; ok { - ris.MapInputSource.valueMap[name] = bogus{0} - } - return true -} - -func TestGenericApplyInputSourceValue_Alias(t *testing.T) { - v := &Parser{"abc", "def"} - tis := testApplyInputSource{ - Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Aliases: []string{"test_alias"}, Value: &Parser{}}), - FlagName: "test_alias", - MapValue: v, - } - c := runTest(t, tis) - expect(t, v, c.Generic("test_alias")) - - c = runRacyTest(t, tis) - refute(t, v, c.Generic("test_alias")) -} - -func TestGenericApplyInputSourceValue(t *testing.T) { - v := &Parser{"abc", "def"} - tis := testApplyInputSource{ - Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &Parser{}}), - FlagName: "test", - MapValue: v, - } - c := runTest(t, tis) - expect(t, v, c.Generic("test")) - - c = runRacyTest(t, tis) - refute(t, v, c.Generic("test")) -} - -func TestGenericApplyInputSourceMethodContextSet(t *testing.T) { - p := &Parser{"abc", "def"} - tis := testApplyInputSource{ - Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Value: &Parser{}}), - FlagName: "test", - MapValue: &Parser{"efg", "hig"}, - ContextValueString: p.String(), - } - c := runTest(t, tis) - expect(t, p, c.Generic("test")) - - c = runRacyTest(t, tis) - refute(t, p, c.Generic("test")) -} - -func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewGenericFlag(&cli.GenericFlag{ - Name: "test", - Value: &Parser{}, - EnvVars: []string{"TEST"}, - }), - FlagName: "test", - MapValue: &Parser{"efg", "hij"}, - EnvVarName: "TEST", - EnvVarValue: "abc,def", - } - c := runTest(t, tis) - expect(t, &Parser{"abc", "def"}, c.Generic("test")) - - c = runRacyTest(t, tis) - refute(t, &Parser{"abc", "def"}, c.Generic("test")) -} - -func TestStringSliceApplyInputSourceValue_Alias(t *testing.T) { - dest := cli.NewStringSlice() - tis := testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), - FlagName: "test_alias", - MapValue: []interface{}{"hello", "world"}, - } - c := runTest(t, tis) - expect(t, c.StringSlice("test_alias"), []string{"hello", "world"}) - expect(t, dest.Value(), []string{"hello", "world"}) - - // reset dest - dest = cli.NewStringSlice() - tis = testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), - FlagName: "test_alias", - MapValue: []interface{}{"hello", "world"}, - } - c = runRacyTest(t, tis) - refute(t, c.StringSlice("test_alias"), []string{"hello", "world"}) - refute(t, dest.Value(), []string{"hello", "world"}) -} - -func TestStringSliceApplyInputSourceValue(t *testing.T) { - dest := cli.NewStringSlice() - tis := testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{"hello", "world"}, - } - c := runTest(t, tis) - expect(t, c.StringSlice("test"), []string{"hello", "world"}) - expect(t, dest.Value(), []string{"hello", "world"}) - - // reset dest - dest = cli.NewStringSlice() - tis = testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{"hello", "world"}, - } - c = runRacyTest(t, tis) - refute(t, c.StringSlice("test"), []string{"hello", "world"}) - refute(t, dest.Value(), []string{"hello", "world"}) -} - -func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) { - dest := cli.NewStringSlice() - c := runTest(t, testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{"hello", "world"}, - ContextValueString: "ohno", - }) - expect(t, c.StringSlice("test"), []string{"ohno"}) - expect(t, dest.Value(), []string{"ohno"}) -} - -func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: []interface{}{"hello", "world"}, - EnvVarName: "TEST", - EnvVarValue: "oh,no", - } - c := runTest(t, tis) - expect(t, c.StringSlice("test"), []string{"oh", "no"}) - - c = runRacyTest(t, tis) - refute(t, c.StringSlice("test"), []string{"oh", "no"}) -} - -func TestIntSliceApplyInputSourceValue_Alias(t *testing.T) { - dest := cli.NewIntSlice() - tis := testApplyInputSource{ - Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), - FlagName: "test_alias", - MapValue: []interface{}{1, 2}, - } - c := runTest(t, tis) - expect(t, c.IntSlice("test_alias"), []int{1, 2}) - expect(t, dest.Value(), []int{1, 2}) - - dest = cli.NewIntSlice() - tis = testApplyInputSource{ - Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), - FlagName: "test_alias", - MapValue: []interface{}{1, 2}, - } - c = runRacyTest(t, tis) - refute(t, c.IntSlice("test_alias"), []int{1, 2}) - refute(t, dest.Value(), []int{1, 2}) -} - -func TestIntSliceApplyInputSourceValue(t *testing.T) { - dest := cli.NewIntSlice() - tis := testApplyInputSource{ - Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{1, 2}, - } - c := runTest(t, tis) - expect(t, c.IntSlice("test"), []int{1, 2}) - expect(t, dest.Value(), []int{1, 2}) - - // reset dest - dest = cli.NewIntSlice() - tis = testApplyInputSource{ - Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{1, 2}, - } - c = runRacyTest(t, tis) - refute(t, c.IntSlice("test"), []int{1, 2}) - refute(t, dest.Value(), []int{1, 2}) -} - -func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) { - dest := cli.NewIntSlice() - tis := testApplyInputSource{ - Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{1, 2}, - ContextValueString: "3", - } - c := runTest(t, tis) - expect(t, c.IntSlice("test"), []int{3}) - expect(t, dest.Value(), []int{3}) - - // reset dest - dest = cli.NewIntSlice() - tis = testApplyInputSource{ - Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{1, 2}, - ContextValueString: "3", - } - c = runRacyTest(t, tis) - refute(t, c.IntSlice("test"), []int{3}) - refute(t, dest.Value(), []int{3}) -} - -func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: []interface{}{1, 2}, - EnvVarName: "TEST", - EnvVarValue: "3,4", - } - c := runTest(t, tis) - expect(t, c.IntSlice("test"), []int{3, 4}) - - c = runRacyTest(t, tis) - refute(t, c.IntSlice("test"), []int{3, 4}) -} - -func TestInt64SliceFlagApplyInputSourceValue(t *testing.T) { - dest := cli.NewInt64Slice() - tis := testApplyInputSource{ - Flag: NewInt64SliceFlag(&cli.Int64SliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{int64(1), int64(2)}, - } - c := runTest(t, tis) - expect(t, c.Int64Slice("test"), []int64{1, 2}) - expect(t, dest.Value(), []int64{1, 2}) - - // reset dest - dest = cli.NewInt64Slice() - tis = testApplyInputSource{ - Flag: NewInt64SliceFlag(&cli.Int64SliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{int64(1), int64(2)}, - } - c = runRacyTest(t, tis) - refute(t, c.IntSlice("test"), []int64{1, 2}) - refute(t, dest.Value(), []int64{1, 2}) -} - -func TestBoolApplyInputSourceMethodSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}), - FlagName: "test", - MapValue: true, - } - c := runTest(t, tis) - expect(t, true, c.Bool("test")) - - c = runRacyTest(t, tis) - refute(t, true, c.Bool("test")) -} - -func TestBoolApplyInputSourceMethodSet_Alias(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewBoolFlag(&cli.BoolFlag{Name: "test", Aliases: []string{"test_alias"}}), - FlagName: "test_alias", - MapValue: true, - } - c := runTest(t, tis) - expect(t, true, c.Bool("test_alias")) - - c = runRacyTest(t, tis) - refute(t, true, c.Bool("test_alias")) -} - -func TestBoolApplyInputSourceMethodContextSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}), - FlagName: "test", - MapValue: false, - ContextValueString: "true", - } - c := runTest(t, tis) - expect(t, true, c.Bool("test")) - - c = runRacyTest(t, tis) - refute(t, true, c.Bool("test")) -} - -func TestBoolApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewBoolFlag(&cli.BoolFlag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: false, - EnvVarName: "TEST", - EnvVarValue: "true", - } - c := runTest(t, tis) - expect(t, true, c.Bool("test")) - - c = runRacyTest(t, tis) - refute(t, true, c.Bool("test")) -} - -func TestStringApplyInputSourceMethodSet_Alias(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewStringFlag(&cli.StringFlag{Name: "test", Aliases: []string{"test_alias"}}), - FlagName: "test_alias", - MapValue: "hello", - } - c := runTest(t, tis) - expect(t, "hello", c.String("test_alias")) - - c = runRacyTest(t, tis) - refute(t, "hello", c.String("test_alias")) -} - -func TestStringApplyInputSourceMethodSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewStringFlag(&cli.StringFlag{Name: "test"}), - FlagName: "test", - MapValue: "hello", - } - c := runTest(t, tis) - expect(t, "hello", c.String("test")) - - c = runRacyTest(t, tis) - refute(t, "hello", c.String("test")) -} - -func TestStringApplyInputSourceMethodContextSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewStringFlag(&cli.StringFlag{Name: "test"}), - FlagName: "test", - MapValue: "hello", - ContextValueString: "goodbye", - } - c := runTest(t, tis) - expect(t, "goodbye", c.String("test")) - - c = runRacyTest(t, tis) - refute(t, "goodbye", c.String("test")) -} - -func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewStringFlag(&cli.StringFlag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: "hello", - EnvVarName: "TEST", - EnvVarValue: "goodbye", - } - c := runTest(t, tis) - expect(t, "goodbye", c.String("test")) - - c = runRacyTest(t, tis) - refute(t, "goodbye", c.String("test")) -} - -func TestPathApplyInputSourceMethodSet_Alias(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewPathFlag(&cli.PathFlag{Name: "test", Aliases: []string{"test_alias"}}), - FlagName: "test_alias", - MapValue: "hello", - SourcePath: "/path/to/source/file", - } - c := runTest(t, tis) - - expected := "/path/to/source/hello" - if runtime.GOOS == "windows" { - var err error - // Prepend the corresponding drive letter (or UNC path?), and change - // to windows-style path: - expected, err = filepath.Abs(expected) - if err != nil { - t.Fatal(err) - } - } - expect(t, expected, c.String("test_alias")) - - c = runRacyTest(t, tis) - refute(t, expected, c.String("test_alias")) -} - -func TestPathApplyInputSourceMethodSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewPathFlag(&cli.PathFlag{Name: "test"}), - FlagName: "test", - MapValue: "hello", - SourcePath: "/path/to/source/file", - } - c := runTest(t, tis) - - expected := "/path/to/source/hello" - if runtime.GOOS == "windows" { - var err error - // Prepend the corresponding drive letter (or UNC path?), and change - // to windows-style path: - expected, err = filepath.Abs(expected) - if err != nil { - t.Fatal(err) - } - } - expect(t, expected, c.String("test")) - - c = runRacyTest(t, tis) - refute(t, expected, c.String("test")) -} - -func TestPathApplyInputSourceMethodContextSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewPathFlag(&cli.PathFlag{Name: "test"}), - FlagName: "test", - MapValue: "hello", - ContextValueString: "goodbye", - SourcePath: "/path/to/source/file", - } - c := runTest(t, tis) - expect(t, "goodbye", c.String("test")) - - c = runRacyTest(t, tis) - refute(t, "goodbye", c.String("test")) -} - -func TestPathApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewPathFlag(&cli.PathFlag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: "hello", - EnvVarName: "TEST", - EnvVarValue: "goodbye", - SourcePath: "/path/to/source/file", - } - c := runTest(t, tis) - expect(t, "goodbye", c.String("test")) - - c = runRacyTest(t, tis) - refute(t, "goodbye", c.String("test")) -} - -func TestIntApplyInputSourceMethodSet_Alias(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewIntFlag(&cli.IntFlag{Name: "test", Aliases: []string{"test_alias"}}), - FlagName: "test_alias", - MapValue: 15, - } - c := runTest(t, tis) - expect(t, 15, c.Int("test_alias")) - - c = runRacyTest(t, tis) - refute(t, 15, c.Int("test_alias")) -} - -func TestIntApplyInputSourceMethodSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), - FlagName: "test", - MapValue: 15, - } - c := runTest(t, tis) - expect(t, 15, c.Int("test")) - - c = runRacyTest(t, tis) - refute(t, 15, c.Int("test")) -} - -func TestIntApplyInputSourceMethodSetNegativeValue(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), - FlagName: "test", - MapValue: -1, - } - c := runTest(t, tis) - expect(t, -1, c.Int("test")) - - c = runRacyTest(t, tis) - refute(t, -1, c.Int("test")) -} - -func TestIntApplyInputSourceMethodContextSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), - FlagName: "test", - MapValue: 15, - ContextValueString: "7", - } - c := runTest(t, tis) - expect(t, 7, c.Int("test")) - - c = runRacyTest(t, tis) - refute(t, 7, c.Int("test")) -} - -func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: 15, - EnvVarName: "TEST", - EnvVarValue: "12", - } - c := runTest(t, tis) - expect(t, 12, c.Int("test")) - - c = runRacyTest(t, tis) - refute(t, 12, c.Int("test")) -} - -func TestDurationApplyInputSourceMethodSet_Alias(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewDurationFlag(&cli.DurationFlag{Name: "test", Aliases: []string{"test_alias"}}), - FlagName: "test_alias", - MapValue: 30 * time.Second, - } - c := runTest(t, tis) - expect(t, 30*time.Second, c.Duration("test_alias")) - - c = runRacyTest(t, tis) - refute(t, 30*time.Second, c.Duration("test_alias")) -} - -func TestDurationApplyInputSourceMethodSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}), - FlagName: "test", - MapValue: 30 * time.Second, - } - c := runTest(t, tis) - expect(t, 30*time.Second, c.Duration("test")) - - c = runRacyTest(t, tis) - refute(t, 30*time.Second, c.Duration("test")) -} - -func TestDurationApplyInputSourceMethodSetNegativeValue(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}), - FlagName: "test", - MapValue: -30 * time.Second, - } - c := runTest(t, tis) - expect(t, -30*time.Second, c.Duration("test")) - - c = runRacyTest(t, tis) - refute(t, -30*time.Second, c.Duration("test")) -} - -func TestDurationApplyInputSourceMethodContextSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}), - FlagName: "test", - MapValue: 30 * time.Second, - ContextValueString: (15 * time.Second).String(), - } - c := runTest(t, tis) - expect(t, 15*time.Second, c.Duration("test")) - - c = runRacyTest(t, tis) - refute(t, 15*time.Second, c.Duration("test")) -} - -func TestDurationApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewDurationFlag(&cli.DurationFlag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: 30 * time.Second, - EnvVarName: "TEST", - EnvVarValue: (15 * time.Second).String(), - } - c := runTest(t, tis) - expect(t, 15*time.Second, c.Duration("test")) - - c = runRacyTest(t, tis) - refute(t, 15*time.Second, c.Duration("test")) -} - -func TestFloat64ApplyInputSourceMethodSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}), - FlagName: "test", - MapValue: 1.3, - } - c := runTest(t, tis) - expect(t, 1.3, c.Float64("test")) - - c = runRacyTest(t, tis) - refute(t, 1.3, c.Float64("test")) -} - -func TestFloat64ApplyInputSourceMethodSetNegativeValue_Alias(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test", Aliases: []string{"test_alias"}}), - FlagName: "test_alias", - MapValue: -1.3, - } - c := runTest(t, tis) - expect(t, -1.3, c.Float64("test_alias")) - - c = runRacyTest(t, tis) - refute(t, -1.3, c.Float64("test_alias")) -} - -func TestFloat64ApplyInputSourceMethodSetNegativeValue(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}), - FlagName: "test", - MapValue: -1.3, - } - c := runTest(t, tis) - expect(t, -1.3, c.Float64("test")) - - c = runRacyTest(t, tis) - refute(t, -1.3, c.Float64("test")) -} - -func TestFloat64ApplyInputSourceMethodSetNegativeValueNotSet(t *testing.T) { - c := runTest(t, testApplyInputSource{ - Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test1"}), - FlagName: "test1", - // dont set map value - }) - expect(t, 0.0, c.Float64("test1")) -} - -func TestFloat64ApplyInputSourceMethodContextSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}), - FlagName: "test", - MapValue: 1.3, - ContextValueString: fmt.Sprintf("%v", 1.4), - } - c := runTest(t, tis) - expect(t, 1.4, c.Float64("test")) - - c = runRacyTest(t, tis) - refute(t, 1.4, c.Float64("test")) -} - -func TestFloat64ApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: 1.3, - EnvVarName: "TEST", - EnvVarValue: fmt.Sprintf("%v", 1.4), - } - c := runTest(t, tis) - expect(t, 1.4, c.Float64("test")) - - c = runRacyTest(t, tis) - refute(t, 1.4, c.Float64("test")) -} - -func runTest(t *testing.T, test testApplyInputSource) *cli.Context { - inputSource := &MapInputSource{ - file: test.SourcePath, - valueMap: map[interface{}]interface{}{test.FlagName: test.MapValue}, - } - set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError) - c := cli.NewContext(nil, set, nil) - if test.EnvVarName != "" && test.EnvVarValue != "" { - _ = os.Setenv(test.EnvVarName, test.EnvVarValue) - defer os.Setenv(test.EnvVarName, "") - } - - _ = test.Flag.Apply(set) - if test.ContextValue != nil { - f := set.Lookup(test.FlagName) - f.Value = test.ContextValue - } - if test.ContextValueString != "" { - _ = set.Set(test.FlagName, test.ContextValueString) - } - _ = test.Flag.ApplyInputSourceValue(c, inputSource) - - return c -} - -func runRacyTest(t *testing.T, test testApplyInputSource) *cli.Context { - set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError) - c := cli.NewContext(nil, set, nil) - _ = test.Flag.ApplyInputSourceValue(c, &racyInputSource{ - MapInputSource: &MapInputSource{ - file: test.SourcePath, - valueMap: map[interface{}]interface{}{test.FlagName: test.MapValue}, - }, - }) - - return c -} - -type Parser [2]string - -func (p *Parser) Set(value string) error { - parts := strings.Split(value, ",") - if len(parts) != 2 { - return fmt.Errorf("invalid format") - } - - (*p)[0] = parts[0] - (*p)[1] = parts[1] - - return nil -} - -func (p *Parser) String() string { - return fmt.Sprintf("%s,%s", p[0], p[1]) -} - -type bogus [1]uint diff --git a/go.mod b/go.mod index 5cd5402..c939294 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,16 @@ module github.com/urfave/cli-altsrc/v3 go 1.18 require ( - github.com/BurntSushi/toml v1.2.1 - github.com/urfave/cli/v3 v3.0.0-20221108200634-766786fcc204 + github.com/BurntSushi/toml v1.3.2 + github.com/stretchr/testify v1.8.4 + github.com/urfave/cli/v3 v3.0.0-alpha4 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect ) diff --git a/go.sum b/go.sum index 0008b0e..462cae7 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,17 @@ -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/urfave/cli/v3 v3.0.0-20221108200634-766786fcc204 h1:5aw+jYRBfOZJJsagagqBOvwShH5vHaSyJUmEKc/wX5o= -github.com/urfave/cli/v3 v3.0.0-20221108200634-766786fcc204/go.mod h1:o9y/j7PxPajDAEl+kKAdwePXiN/ZA5IDRjCCa8/Wu6s= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v3 v3.0.0-alpha4 h1:RJFGIs3mcalmc2YgliDh0Pa4l79S+Dqdz7cW8Fcp7Rg= +github.com/urfave/cli/v3 v3.0.0-alpha4/go.mod h1:ZFqSEHhze0duJACOdz43I5IcnKhf4RoTlOoUMBUggOI= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/helpers_test.go b/helpers_test.go deleted file mode 100644 index 1f8d5c2..0000000 --- a/helpers_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package altsrc - -import ( - "os" - "reflect" - "runtime" - "strings" - "testing" -) - -var ( - wd, _ = os.Getwd() -) - -func expect(t *testing.T, a interface{}, b interface{}) { - _, fn, line, _ := runtime.Caller(1) - fn = strings.Replace(fn, wd+"/", "", -1) - - if !reflect.DeepEqual(a, b) { - t.Errorf("(%s:%d) Expected %v (type %v) - Got %v (type %v)", fn, line, b, reflect.TypeOf(b), a, reflect.TypeOf(a)) - } -} - -func refute(t *testing.T, a interface{}, b interface{}) { - _, fn, line, _ := runtime.Caller(1) - fn = strings.Replace(fn, wd+"/", "", -1) - - if reflect.DeepEqual(a, b) { - t.Errorf("(%s:%d) Did not expect %v (type %v) - Got %v (type %v)", fn, line, b, reflect.TypeOf(b), a, reflect.TypeOf(a)) - } -} diff --git a/input_source_context.go b/input_source_context.go deleted file mode 100644 index 4cd8392..0000000 --- a/input_source_context.go +++ /dev/null @@ -1,28 +0,0 @@ -package altsrc - -import ( - "time" - - "github.com/urfave/cli/v3" -) - -// InputSourceContext is an interface used to allow -// other input sources to be implemented as needed. -// -// Source returns an identifier for the input source. In case of file source -// it should return path to the file. -type InputSourceContext interface { - Source() string - - Int(name string) (int, error) - Duration(name string) (time.Duration, error) - Float64(name string) (float64, error) - String(name string) (string, error) - StringSlice(name string) ([]string, error) - IntSlice(name string) ([]int, error) - Int64Slice(name string) ([]int64, error) - Generic(name string) (cli.Generic, error) - Bool(name string) (bool, error) - - isSet(name string) bool -} diff --git a/internal/package.go b/internal/package.go new file mode 100644 index 0000000..a1390a4 --- /dev/null +++ b/internal/package.go @@ -0,0 +1,26 @@ +package internal + +import ( + "context" + "os/exec" + "path/filepath" + "strings" +) + +func MustTestdataDir(ctx context.Context) string { + testdataDir, err := TestdataDir(ctx) + if err != nil { + panic(err) + } + + return testdataDir +} + +func TestdataDir(ctx context.Context) (string, error) { + topBytes, err := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel").Output() + if err != nil { + return "", err + } + + return filepath.Join(strings.TrimSpace(string(topBytes)), ".testdata"), nil +} diff --git a/json_command_test.go b/json_command_test.go deleted file mode 100644 index e39dabc..0000000 --- a/json_command_test.go +++ /dev/null @@ -1,329 +0,0 @@ -package altsrc - -import ( - "flag" - "io/ioutil" - "os" - "testing" - - "github.com/urfave/cli/v3" -) - -const ( - fileName = "current.json" - simpleJSON = `{"test": 15, "testb": false}` - nestedJSON = `{"top": {"test": 15}}` -) - -func TestCommandJSONFileTest(t *testing.T) { - cleanup := writeTempFile(t, fileName, simpleJSON) - defer cleanup() - - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - test := []string{"test-cmd", "--load", fileName} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - - valb := c.Bool("testb") - expect(t, valb, false) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test"}), - &cli.StringFlag{Name: "load"}, - NewBoolFlag(&cli.BoolFlag{Name: "testb", Value: true}), - }, - } - command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandJSONFileTestGlobalEnvVarWins(t *testing.T) { - cleanup := writeTempFile(t, fileName, simpleJSON) - defer cleanup() - - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", fileName} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandJSONFileTestGlobalEnvVarWinsNested(t *testing.T) { - cleanup := writeTempFile(t, fileName, nestedJSON) - defer cleanup() - - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", fileName} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test", EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandJSONFileTestSpecifiedFlagWins(t *testing.T) { - cleanup := writeTempFile(t, fileName, simpleJSON) - defer cleanup() - - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - test := []string{"test-cmd", "--load", fileName, "--test", "7"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test"}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandJSONFileTestSpecifiedFlagWinsNested(t *testing.T) { - cleanup := writeTempFile(t, fileName, nestedJSON) - defer cleanup() - - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - test := []string{"test-cmd", "--load", fileName, "--top.test", "7"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test"}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandJSONFileTestDefaultValueFileWins(t *testing.T) { - cleanup := writeTempFile(t, fileName, simpleJSON) - defer cleanup() - - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - test := []string{"test-cmd", "--load", fileName} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test", Value: 7}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandJSONFileTestDefaultValueFileWinsNested(t *testing.T) { - cleanup := writeTempFile(t, fileName, nestedJSON) - defer cleanup() - - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - test := []string{"test-cmd", "--load", fileName} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandJSONFileFlagHasDefaultGlobalEnvJSONSetGlobalEnvWins(t *testing.T) { - cleanup := writeTempFile(t, fileName, simpleJSON) - defer cleanup() - - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", fileName} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test", Value: 7, EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandJSONFileFlagHasDefaultGlobalEnvJSONSetGlobalEnvWinsNested(t *testing.T) { - cleanup := writeTempFile(t, fileName, nestedJSON) - defer cleanup() - - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", fileName} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7, EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func writeTempFile(t *testing.T, name string, content string) func() { - if err := ioutil.WriteFile(name, []byte(content), 0666); err != nil { - t.Fatalf("cannot write %q: %v", name, err) - } - return func() { - if err := os.Remove(name); err != nil { - t.Errorf("cannot remove %q: %v", name, err) - } - } -} diff --git a/json_source_context.go b/json_source_context.go deleted file mode 100644 index 5589d59..0000000 --- a/json_source_context.go +++ /dev/null @@ -1,240 +0,0 @@ -package altsrc - -import ( - "encoding/json" - "fmt" - "io" - "io/ioutil" - "strings" - "time" - - "github.com/urfave/cli/v3" -) - -// NewJSONSourceFromFlagFunc returns a func that takes a cli.Context -// and returns an InputSourceContext suitable for retrieving config -// variables from a file containing JSON data with the file name defined -// by the given flag. -func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceContext, error) { - return func(cCtx *cli.Context) (InputSourceContext, error) { - if cCtx.IsSet(flag) { - return NewJSONSourceFromFile(cCtx.String(flag)) - } - - return defaultInputSource() - } -} - -// NewJSONSourceFromFile returns an InputSourceContext suitable for -// retrieving config variables from a file (or url) containing JSON -// data. -func NewJSONSourceFromFile(f string) (InputSourceContext, error) { - data, err := loadDataFrom(f) - if err != nil { - return nil, err - } - - return NewJSONSource(data) -} - -// NewJSONSourceFromReader returns an InputSourceContext suitable for -// retrieving config variables from an io.Reader that returns JSON data. -func NewJSONSourceFromReader(r io.Reader) (InputSourceContext, error) { - data, err := ioutil.ReadAll(r) - if err != nil { - return nil, err - } - return NewJSONSource(data) -} - -// NewJSONSource returns an InputSourceContext suitable for retrieving -// config variables from raw JSON data. -func NewJSONSource(data []byte) (InputSourceContext, error) { - var deserialized map[string]interface{} - if err := json.Unmarshal(data, &deserialized); err != nil { - return nil, err - } - return &jsonSource{deserialized: deserialized}, nil -} - -func (x *jsonSource) Source() string { - return x.file -} - -func (x *jsonSource) Int(name string) (int, error) { - i, err := x.getValue(name) - if err != nil { - return 0, err - } - switch v := i.(type) { - default: - return 0, fmt.Errorf("unexpected type %T for %q", i, name) - case int: - return v, nil - case float32: - return int(v), nil - case float64: - return int(v), nil - } -} - -func (x *jsonSource) Duration(name string) (time.Duration, error) { - i, err := x.getValue(name) - if err != nil { - return 0, err - } - v, ok := i.(time.Duration) - if !ok { - return 0, fmt.Errorf("unexpected type %T for %q", i, name) - } - return v, nil -} - -func (x *jsonSource) Float64(name string) (float64, error) { - i, err := x.getValue(name) - if err != nil { - return 0, err - } - v, ok := i.(float64) - if !ok { - return 0, fmt.Errorf("unexpected type %T for %q", i, name) - } - return v, nil -} - -func (x *jsonSource) String(name string) (string, error) { - i, err := x.getValue(name) - if err != nil { - return "", err - } - v, ok := i.(string) - if !ok { - return "", fmt.Errorf("unexpected type %T for %q", i, name) - } - return v, nil -} - -func (x *jsonSource) StringSlice(name string) ([]string, error) { - i, err := x.getValue(name) - if err != nil { - return nil, err - } - switch v := i.(type) { - default: - return nil, fmt.Errorf("unexpected type %T for %q", i, name) - case []string: - return v, nil - case []interface{}: - c := []string{} - for _, s := range v { - if str, ok := s.(string); ok { - c = append(c, str) - } else { - return c, fmt.Errorf("unexpected item type %T in %T for %q", s, c, name) - } - } - return c, nil - } -} - -func (x *jsonSource) IntSlice(name string) ([]int, error) { - i, err := x.getValue(name) - if err != nil { - return nil, err - } - switch v := i.(type) { - default: - return nil, fmt.Errorf("unexpected type %T for %q", i, name) - case []int: - return v, nil - case []interface{}: - c := []int{} - for _, s := range v { - if i2, ok := s.(int); ok { - c = append(c, i2) - } else { - return c, fmt.Errorf("unexpected item type %T in %T for %q", s, c, name) - } - } - return c, nil - } -} - -func (x *jsonSource) Int64Slice(name string) ([]int64, error) { - i, err := x.getValue(name) - if err != nil { - return nil, err - } - switch v := i.(type) { - default: - return nil, fmt.Errorf("unexpected type %T for %q", i, name) - case []int64: - return v, nil - case []interface{}: - c := []int64{} - for _, s := range v { - if i2, ok := s.(int64); ok { - c = append(c, i2) - } else { - return c, fmt.Errorf("unexpected item type %T in %T for %q", s, c, name) - } - } - return c, nil - } -} - -func (x *jsonSource) Generic(name string) (cli.Generic, error) { - i, err := x.getValue(name) - if err != nil { - return nil, err - } - v, ok := i.(cli.Generic) - if !ok { - return nil, fmt.Errorf("unexpected type %T for %q", i, name) - } - return v, nil -} - -func (x *jsonSource) Bool(name string) (bool, error) { - i, err := x.getValue(name) - if err != nil { - return false, err - } - v, ok := i.(bool) - if !ok { - return false, fmt.Errorf("unexpected type %T for %q", i, name) - } - return v, nil -} - -func (x *jsonSource) isSet(name string) bool { - _, err := x.getValue(name) - return err == nil -} - -func (x *jsonSource) getValue(key string) (interface{}, error) { - return jsonGetValue(key, x.deserialized) -} - -func jsonGetValue(key string, m map[string]interface{}) (interface{}, error) { - var ret interface{} - var ok bool - working := m - keys := strings.Split(key, ".") - for ix, k := range keys { - if ret, ok = working[k]; !ok { - return ret, fmt.Errorf("missing key %q", key) - } - if working, ok = ret.(map[string]interface{}); !ok { - if ix < len(keys)-1 { - return ret, fmt.Errorf("unexpected intermediate value at %q segment of %q: %T", k, key, ret) - } - } - } - return ret, nil -} - -type jsonSource struct { - file string - deserialized map[string]interface{} -} diff --git a/json_value_source.go b/json_value_source.go new file mode 100644 index 0000000..33ab202 --- /dev/null +++ b/json_value_source.go @@ -0,0 +1,9 @@ +package altsrc + +import "github.com/urfave/cli/v3" + +// JSON is a helper function that wraps the YAML helper function +// and loads via yaml.Unmarshal +func JSON(key string, paths ...string) cli.ValueSourceChain { + return YAML(key, paths...) +} diff --git a/json_value_source_test.go b/json_value_source_test.go new file mode 100644 index 0000000..507f1e4 --- /dev/null +++ b/json_value_source_test.go @@ -0,0 +1,29 @@ +package altsrc + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestJSON(t *testing.T) { + r := require.New(t) + + configPath := filepath.Join(testdataDir, "test_config.json") + altConfigPath := filepath.Join(testdataDir, "test_alt_config.json") + + vsc := YAML( + "water_fountain.water", + "/dev/null/nonexistent.json", + configPath, + altConfigPath, + ) + v, ok := vsc.Lookup() + r.Equal("false", v) + r.True(ok) + + yvs := vsc.Chain[0].(*yamlValueSource) + r.Equal("yaml file \"/dev/null/nonexistent.json\" at key \"water_fountain.water\"", yvs.String()) + r.Equal("&yamlValueSource{file:\"/dev/null/nonexistent.json\",keyPath:\"water_fountain.water\"}", yvs.GoString()) +} diff --git a/map_input_source.go b/map_input_source.go deleted file mode 100644 index 4bac92c..0000000 --- a/map_input_source.go +++ /dev/null @@ -1,300 +0,0 @@ -package altsrc - -import ( - "fmt" - "reflect" - "strings" - "time" - - "github.com/urfave/cli/v3" -) - -// MapInputSource implements InputSourceContext to return -// data from the map that is loaded. -type MapInputSource struct { - file string - valueMap map[interface{}]interface{} -} - -// NewMapInputSource creates a new MapInputSource for implementing custom input sources. -func NewMapInputSource(file string, valueMap map[interface{}]interface{}) *MapInputSource { - return &MapInputSource{file: file, valueMap: valueMap} -} - -// nestedVal checks if the name has '.' delimiters. -// If so, it tries to traverse the tree by the '.' delimited sections to find -// a nested value for the key. -func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool) { - if sections := strings.Split(name, "."); len(sections) > 1 { - node := tree - for _, section := range sections[:len(sections)-1] { - child, ok := node[section] - if !ok { - return nil, false - } - - switch child := child.(type) { - case map[string]interface{}: - node = make(map[interface{}]interface{}, len(child)) - for k, v := range child { - node[k] = v - } - case map[interface{}]interface{}: - node = child - default: - return nil, false - } - } - if val, ok := node[sections[len(sections)-1]]; ok { - return val, true - } - } - return nil, false -} - -// Source returns the path of the source file -func (fsm *MapInputSource) Source() string { - return fsm.file -} - -// Int returns an int from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Int(name string) (int, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(int) - if !isType { - return 0, incorrectTypeForFlagError(name, "int", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(int) - if !isType { - return 0, incorrectTypeForFlagError(name, "int", nestedGenericValue) - } - return otherValue, nil - } - - return 0, nil -} - -// Duration returns a duration from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Duration(name string) (time.Duration, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - return castDuration(name, otherGenericValue) - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - return castDuration(name, nestedGenericValue) - } - - return 0, nil -} - -func castDuration(name string, value interface{}) (time.Duration, error) { - if otherValue, isType := value.(time.Duration); isType { - return otherValue, nil - } - otherStringValue, isType := value.(string) - parsedValue, err := time.ParseDuration(otherStringValue) - if !isType || err != nil { - return 0, incorrectTypeForFlagError(name, "duration", value) - } - return parsedValue, nil -} - -// Float64 returns an float64 from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Float64(name string) (float64, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(float64) - if !isType { - return 0, incorrectTypeForFlagError(name, "float64", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(float64) - if !isType { - return 0, incorrectTypeForFlagError(name, "float64", nestedGenericValue) - } - return otherValue, nil - } - - return 0, nil -} - -// String returns a string from the map if it exists otherwise returns an empty string -func (fsm *MapInputSource) String(name string) (string, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(string) - if !isType { - return "", incorrectTypeForFlagError(name, "string", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(string) - if !isType { - return "", incorrectTypeForFlagError(name, "string", nestedGenericValue) - } - return otherValue, nil - } - - return "", nil -} - -// StringSlice returns an []string from the map if it exists otherwise returns nil -func (fsm *MapInputSource) StringSlice(name string) ([]string, error) { - otherGenericValue, exists := fsm.valueMap[name] - if !exists { - otherGenericValue, exists = nestedVal(name, fsm.valueMap) - if !exists { - return nil, nil - } - } - - otherValue, isType := otherGenericValue.([]interface{}) - if !isType { - return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) - } - - var stringSlice = make([]string, 0, len(otherValue)) - for i, v := range otherValue { - stringValue, isType := v.(string) - - if !isType { - return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "string", v) - } - - stringSlice = append(stringSlice, stringValue) - } - - return stringSlice, nil -} - -// IntSlice returns an []int from the map if it exists otherwise returns nil -func (fsm *MapInputSource) IntSlice(name string) ([]int, error) { - otherGenericValue, exists := fsm.valueMap[name] - if !exists { - otherGenericValue, exists = nestedVal(name, fsm.valueMap) - if !exists { - return nil, nil - } - } - - otherValue, isType := otherGenericValue.([]interface{}) - if !isType { - return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) - } - - var intSlice = make([]int, 0, len(otherValue)) - for i, v := range otherValue { - intValue, isType := v.(int) - - if !isType { - return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v) - } - - intSlice = append(intSlice, intValue) - } - - return intSlice, nil -} - -// Int64Slice returns an []int64 from the map if it exists otherwise returns nil -func (fsm *MapInputSource) Int64Slice(name string) ([]int64, error) { - otherGenericValue, exists := fsm.valueMap[name] - if !exists { - otherGenericValue, exists = nestedVal(name, fsm.valueMap) - if !exists { - return nil, nil - } - } - - otherValue, isType := otherGenericValue.([]interface{}) - if !isType { - return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) - } - - var int64Slice = make([]int64, 0, len(otherValue)) - for i, v := range otherValue { - int64Value, isType := v.(int64) - - if !isType { - return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v) - } - - int64Slice = append(int64Slice, int64Value) - } - - return int64Slice, nil -} - -// Generic returns an cli.Generic from the map if it exists otherwise returns nil -func (fsm *MapInputSource) Generic(name string) (cli.Generic, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(cli.Generic) - if !isType { - return nil, incorrectTypeForFlagError(name, "cli.Generic", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(cli.Generic) - if !isType { - return nil, incorrectTypeForFlagError(name, "cli.Generic", nestedGenericValue) - } - return otherValue, nil - } - - return nil, nil -} - -// Bool returns an bool from the map otherwise returns false -func (fsm *MapInputSource) Bool(name string) (bool, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(bool) - if !isType { - return false, incorrectTypeForFlagError(name, "bool", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(bool) - if !isType { - return false, incorrectTypeForFlagError(name, "bool", nestedGenericValue) - } - return otherValue, nil - } - - return false, nil -} - -func (fsm *MapInputSource) isSet(name string) bool { - if _, exists := fsm.valueMap[name]; exists { - return exists - } - - _, exists := nestedVal(name, fsm.valueMap) - return exists -} - -func incorrectTypeForFlagError(name, expectedTypeName string, value interface{}) error { - valueType := reflect.TypeOf(value) - valueTypeName := "" - if valueType != nil { - valueTypeName = valueType.Name() - } - - return fmt.Errorf("Mismatched type for flag '%s'. Expected '%s' but actual is '%s'", name, expectedTypeName, valueTypeName) -} diff --git a/map_input_source_test.go b/map_input_source_test.go deleted file mode 100644 index cf399b5..0000000 --- a/map_input_source_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package altsrc - -import ( - "testing" - "time" -) - -func TestMapDuration(t *testing.T) { - inputSource := NewMapInputSource( - "test", - map[interface{}]interface{}{ - "duration_of_duration_type": time.Minute, - "duration_of_string_type": "1m", - "duration_of_int_type": 1000, - }) - d, err := inputSource.Duration("duration_of_duration_type") - expect(t, time.Minute, d) - expect(t, nil, err) - d, err = inputSource.Duration("duration_of_string_type") - expect(t, time.Minute, d) - expect(t, nil, err) - _, err = inputSource.Duration("duration_of_int_type") - refute(t, nil, err) -} - -func TestMapInputSource_Int64Slice(t *testing.T) { - inputSource := NewMapInputSource( - "test", - map[interface{}]interface{}{ - "test_num": []interface{}{int64(1), int64(2), int64(3)}, - }) - d, err := inputSource.Int64Slice("test_num") - expect(t, []int64{1, 2, 3}, d) - expect(t, nil, err) -} diff --git a/testdata/empty.yml b/testdata/empty.yml deleted file mode 100644 index ab2fc5d..0000000 --- a/testdata/empty.yml +++ /dev/null @@ -1 +0,0 @@ -# empty file \ No newline at end of file diff --git a/toml_command_test.go b/toml_command_test.go deleted file mode 100644 index 9e52435..0000000 --- a/toml_command_test.go +++ /dev/null @@ -1,305 +0,0 @@ -package altsrc - -import ( - "flag" - "io/ioutil" - "os" - "testing" - - "github.com/urfave/cli/v3" -) - -func TestCommandTomFileTest(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - test := []string{"test-cmd", "--load", "current.toml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test"}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestGlobalEnvVarWins(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - - _ = os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - test := []string{"test-cmd", "--load", "current.toml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestGlobalEnvVarWinsNested(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) - defer os.Remove("current.toml") - - _ = os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - test := []string{"test-cmd", "--load", "current.toml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test", EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestSpecifiedFlagWins(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - - test := []string{"test-cmd", "--load", "current.toml", "--test", "7"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test"}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestSpecifiedFlagWinsNested(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte(`[top] - test = 15`), 0666) - defer os.Remove("current.toml") - - test := []string{"test-cmd", "--load", "current.toml", "--top.test", "7"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test"}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestDefaultValueFileWins(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - - test := []string{"test-cmd", "--load", "current.toml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test", Value: 7}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandTomlFileTestDefaultValueFileWinsNested(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) - defer os.Remove("current.toml") - - test := []string{"test-cmd", "--load", "current.toml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWins(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) - defer os.Remove("current.toml") - - _ = os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", "current.toml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test", Value: 7, EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWinsNested(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) - defer os.Remove("current.toml") - - _ = os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", "current.toml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7, EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c, test...) - - expect(t, err, nil) -} diff --git a/toml_file_loader.go b/toml_file_loader.go deleted file mode 100644 index 0e614e3..0000000 --- a/toml_file_loader.go +++ /dev/null @@ -1,112 +0,0 @@ -package altsrc - -import ( - "fmt" - "reflect" - - "github.com/BurntSushi/toml" - "github.com/urfave/cli/v3" -) - -type tomlMap struct { - Map map[interface{}]interface{} -} - -func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) { - ret = make(map[interface{}]interface{}) - m := i.(map[string]interface{}) - for key, val := range m { - v := reflect.ValueOf(val) - switch v.Kind() { - case reflect.Bool: - ret[key] = val.(bool) - case reflect.String: - ret[key] = val.(string) - case reflect.Int: - ret[key] = val.(int) - case reflect.Int8: - ret[key] = int(val.(int8)) - case reflect.Int16: - ret[key] = int(val.(int16)) - case reflect.Int32: - ret[key] = int(val.(int32)) - case reflect.Int64: - ret[key] = int(val.(int64)) - case reflect.Uint: - ret[key] = int(val.(uint)) - case reflect.Uint8: - ret[key] = int(val.(uint8)) - case reflect.Uint16: - ret[key] = int(val.(uint16)) - case reflect.Uint32: - ret[key] = int(val.(uint32)) - case reflect.Uint64: - ret[key] = int(val.(uint64)) - case reflect.Float32: - ret[key] = float64(val.(float32)) - case reflect.Float64: - ret[key] = val.(float64) - case reflect.Map: - if tmp, err := unmarshalMap(val); err == nil { - ret[key] = tmp - } else { - return nil, err - } - case reflect.Array, reflect.Slice: - ret[key] = val.([]interface{}) - default: - return nil, fmt.Errorf("Unsupported: type = %#v", v.Kind()) - } - } - return ret, nil -} - -func (tm *tomlMap) UnmarshalTOML(i interface{}) error { - if tmp, err := unmarshalMap(i); err == nil { - tm.Map = tmp - } else { - return err - } - return nil -} - -type tomlSourceContext struct { - FilePath string -} - -// NewTomlSourceFromFile creates a new TOML InputSourceContext from a filepath. -func NewTomlSourceFromFile(file string) (InputSourceContext, error) { - tsc := &tomlSourceContext{FilePath: file} - var results tomlMap = tomlMap{} - if err := readCommandToml(tsc.FilePath, &results); err != nil { - return nil, fmt.Errorf("Unable to load TOML file '%s': inner error: \n'%v'", tsc.FilePath, err.Error()) - } - return &MapInputSource{file: file, valueMap: results.Map}, nil -} - -// NewTomlSourceFromFlagFunc creates a new TOML InputSourceContext from a provided flag name and source context. -func NewTomlSourceFromFlagFunc(flagFileName string) func(cCtx *cli.Context) (InputSourceContext, error) { - return func(cCtx *cli.Context) (InputSourceContext, error) { - if cCtx.IsSet(flagFileName) { - filePath := cCtx.String(flagFileName) - return NewTomlSourceFromFile(filePath) - } - - return defaultInputSource() - } -} - -func readCommandToml(filePath string, container interface{}) (err error) { - b, err := loadDataFrom(filePath) - if err != nil { - return err - } - - err = toml.Unmarshal(b, container) - if err != nil { - return err - } - - err = nil - return -} diff --git a/toml_map.go b/toml_map.go new file mode 100644 index 0000000..ec14047 --- /dev/null +++ b/toml_map.go @@ -0,0 +1,69 @@ +package altsrc + +import ( + "fmt" + "reflect" +) + +type tomlMap struct { + Map map[any]any +} + +func (tm *tomlMap) UnmarshalTOML(i any) error { + if v, err := unmarshalMap(i); err == nil { + tm.Map = v + } else { + return err + } + + return nil +} + +func unmarshalMap(i any) (ret map[any]any, err error) { + ret = make(map[any]any) + m := i.(map[string]any) + for key, val := range m { + v := reflect.ValueOf(val) + switch v.Kind() { + case reflect.Bool: + ret[key] = val.(bool) + case reflect.String: + ret[key] = val.(string) + case reflect.Int: + ret[key] = val.(int) + case reflect.Int8: + ret[key] = int(val.(int8)) + case reflect.Int16: + ret[key] = int(val.(int16)) + case reflect.Int32: + ret[key] = int(val.(int32)) + case reflect.Int64: + ret[key] = int(val.(int64)) + case reflect.Uint: + ret[key] = int(val.(uint)) + case reflect.Uint8: + ret[key] = int(val.(uint8)) + case reflect.Uint16: + ret[key] = int(val.(uint16)) + case reflect.Uint32: + ret[key] = int(val.(uint32)) + case reflect.Uint64: + ret[key] = int(val.(uint64)) + case reflect.Float32: + ret[key] = float64(val.(float32)) + case reflect.Float64: + ret[key] = val.(float64) + case reflect.Map: + if tmp, err := unmarshalMap(val); err == nil { + ret[key] = tmp + } else { + return nil, err + } + case reflect.Array, reflect.Slice: + ret[key] = val.([]any) + default: + return nil, fmt.Errorf("%[1]w: unsupported type %#[2]v", Err, v.Kind()) + } + } + return ret, nil +} diff --git a/toml_value_source.go b/toml_value_source.go new file mode 100644 index 0000000..2bb1a1a --- /dev/null +++ b/toml_value_source.go @@ -0,0 +1,66 @@ +package altsrc + +import ( + "fmt" + + "github.com/BurntSushi/toml" + "github.com/urfave/cli/v3" +) + +// TOML is a helper function to encapsulate a number of +// tomlValueSource together as a cli.ValueSourceChain +func TOML(key string, paths ...string) cli.ValueSourceChain { + vsc := cli.ValueSourceChain{Chain: []cli.ValueSource{}} + + for _, path := range paths { + vsc.Chain = append( + vsc.Chain, + &tomlValueSource{ + file: path, + key: key, + tmc: tomlMapFileSourceCache{ + file: path, + f: tomlUnmarshalFile, + }, + }, + ) + } + + return vsc +} + +type tomlValueSource struct { + file string + key string + + tmc tomlMapFileSourceCache +} + +func (tvs *tomlValueSource) Lookup() (string, bool) { + if v, ok := nestedVal(tvs.key, tvs.tmc.Get().Map); ok { + return fmt.Sprintf("%[1]v", v), ok + } + + return "", false +} + +func (tvs *tomlValueSource) String() string { + return fmt.Sprintf("toml file %[1]q at key %[2]q", tvs.file, tvs.key) +} + +func (tvs *tomlValueSource) GoString() string { + return fmt.Sprintf("&tomlValueSource{file:%[1]q,keyPath:%[2]q}", tvs.file, tvs.key) +} + +func tomlUnmarshalFile(filePath string, container any) error { + b, err := readURI(filePath) + if err != nil { + return err + } + + if err := toml.Unmarshal(b, container); err != nil { + return err + } + + return nil +} diff --git a/toml_value_source_test.go b/toml_value_source_test.go new file mode 100644 index 0000000..53023b3 --- /dev/null +++ b/toml_value_source_test.go @@ -0,0 +1,29 @@ +package altsrc + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTOML(t *testing.T) { + r := require.New(t) + + configPath := filepath.Join(testdataDir, "test_config.toml") + altConfigPath := filepath.Join(testdataDir, "test_alt_config.toml") + + vsc := TOML( + "water_fountain.water", + "/dev/null/nonexistent.toml", + configPath, + altConfigPath, + ) + v, ok := vsc.Lookup() + r.Equal("false", v) + r.True(ok) + + tvs := vsc.Chain[0].(*tomlValueSource) + r.Equal("toml file \"/dev/null/nonexistent.toml\" at key \"water_fountain.water\"", tvs.String()) + r.Equal("&tomlValueSource{file:\"/dev/null/nonexistent.toml\",keyPath:\"water_fountain.water\"}", tvs.GoString()) +} diff --git a/yaml_command_test.go b/yaml_command_test.go deleted file mode 100644 index ed67d80..0000000 --- a/yaml_command_test.go +++ /dev/null @@ -1,308 +0,0 @@ -package altsrc - -import ( - "flag" - "io/ioutil" - "os" - "testing" - - "github.com/urfave/cli/v3" -) - -func TestCommandYamlFileTest(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - test := []string{"test-cmd", "--load", "current.yaml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test"}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - - _ = os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - test := []string{"test-cmd", "--load", "current.yaml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestGlobalEnvVarWinsNested(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.yaml", []byte(`top: - test: 15`), 0666) - defer os.Remove("current.yaml") - - _ = os.Setenv("THE_TEST", "10") - defer os.Setenv("THE_TEST", "") - test := []string{"test-cmd", "--load", "current.yaml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 10) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test", EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - - test := []string{"test-cmd", "--load", "current.yaml", "--test", "7"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test"}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestSpecifiedFlagWinsNested(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.yaml", []byte(`top: - test: 15`), 0666) - defer os.Remove("current.yaml") - - test := []string{"test-cmd", "--load", "current.yaml", "--top.test", "7"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 7) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test"}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - - test := []string{"test-cmd", "--load", "current.yaml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test", Value: 7}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandYamlFileTestDefaultValueFileWinsNested(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.yaml", []byte(`top: - test: 15`), 0666) - defer os.Remove("current.yaml") - - test := []string{"test-cmd", "--load", "current.yaml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 15) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) - defer os.Remove("current.yaml") - - _ = os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", "current.yaml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "test", Value: 7, EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c, test...) - - expect(t, err, nil) -} - -func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWinsNested(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.yaml", []byte(`top: - test: 15`), 0666) - defer os.Remove("current.yaml") - - _ = os.Setenv("THE_TEST", "11") - defer os.Setenv("THE_TEST", "") - - test := []string{"test-cmd", "--load", "current.yaml"} - _ = set.Parse(test) - - c := cli.NewContext(app, set, nil) - - command := &cli.Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(c *cli.Context) error { - val := c.Int("top.test") - expect(t, val, 11) - return nil - }, - Flags: []cli.Flag{ - NewIntFlag(&cli.IntFlag{Name: "top.test", Value: 7, EnvVars: []string{"THE_TEST"}}), - &cli.StringFlag{Name: "load"}}, - } - command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c, test...) - - expect(t, err, nil) -} diff --git a/yaml_file_loader.go b/yaml_file_loader.go deleted file mode 100644 index b1f6c84..0000000 --- a/yaml_file_loader.go +++ /dev/null @@ -1,89 +0,0 @@ -package altsrc - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/url" - "os" - "runtime" - "strings" - - "github.com/urfave/cli/v3" - - "gopkg.in/yaml.v3" -) - -type yamlSourceContext struct { - FilePath string -} - -// NewYamlSourceFromFile creates a new Yaml InputSourceContext from a filepath. -func NewYamlSourceFromFile(file string) (InputSourceContext, error) { - ysc := &yamlSourceContext{FilePath: file} - var results map[interface{}]interface{} - err := readCommandYaml(ysc.FilePath, &results) - if err != nil { - return nil, fmt.Errorf("Unable to load Yaml file '%s': inner error: \n'%v'", ysc.FilePath, err.Error()) - } - - return &MapInputSource{file: file, valueMap: results}, nil -} - -// NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context. -func NewYamlSourceFromFlagFunc(flagFileName string) func(cCtx *cli.Context) (InputSourceContext, error) { - return func(cCtx *cli.Context) (InputSourceContext, error) { - if filePath := cCtx.String(flagFileName); filePath != "" { - return NewYamlSourceFromFile(filePath) - } - return defaultInputSource() - } -} - -func readCommandYaml(filePath string, container interface{}) (err error) { - b, err := loadDataFrom(filePath) - if err != nil { - return err - } - - err = yaml.Unmarshal(b, container) - if err != nil { - return err - } - - err = nil - return -} - -func loadDataFrom(filePath string) ([]byte, error) { - u, err := url.Parse(filePath) - if err != nil { - return nil, err - } - - if u.Host != "" { // i have a host, now do i support the scheme? - switch u.Scheme { - case "http", "https": - res, err := http.Get(filePath) - if err != nil { - return nil, err - } - return ioutil.ReadAll(res.Body) - default: - return nil, fmt.Errorf("scheme of %s is unsupported", filePath) - } - } else if u.Path != "" { // i dont have a host, but I have a path. I am a local file. - if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil { - return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) - } - return ioutil.ReadFile(filePath) - } else if runtime.GOOS == "windows" && strings.Contains(u.String(), "\\") { - // on Windows systems u.Path is always empty, so we need to check the string directly. - if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil { - return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) - } - return ioutil.ReadFile(filePath) - } - - return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) -} diff --git a/yaml_file_loader_test.go b/yaml_file_loader_test.go deleted file mode 100644 index 0505be4..0000000 --- a/yaml_file_loader_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package altsrc_test - -import ( - "fmt" - "log" - "os" - "time" - - altsrc "github.com/urfave/cli-altsrc/v3" - "github.com/urfave/cli/v3" -) - -func ExampleApp_Run_yamlFileLoaderDuration() { - execServe := func(c *cli.Context) error { - keepaliveInterval := c.Duration("keepalive-interval") - fmt.Printf("keepalive %s\n", keepaliveInterval) - return nil - } - - fileExists := func(filename string) bool { - stat, _ := os.Stat(filename) - return stat != nil - } - - // initConfigFileInputSource is like altsrc.InitInputSourceWithContext and altsrc.NewYamlSourceFromFlagFunc, but checks - // if the config flag is exists and only loads it if it does. If the flag is set and the file exists, it fails. - initConfigFileInputSource := func(configFlag string, flags []cli.Flag) cli.BeforeFunc { - return func(context *cli.Context) error { - configFile := context.String(configFlag) - if context.IsSet(configFlag) && !fileExists(configFile) { - return fmt.Errorf("config file %s does not exist", configFile) - } else if !context.IsSet(configFlag) && !fileExists(configFile) { - return nil - } - inputSource, err := altsrc.NewYamlSourceFromFile(configFile) - if err != nil { - return err - } - return altsrc.ApplyInputSourceValues(context, inputSource, flags) - } - } - - flagsServe := []cli.Flag{ - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - EnvVars: []string{"CONFIG_FILE"}, - Value: "testdata/empty.yml", - DefaultText: "testdata/empty.yml", - Usage: "config file", - }, - altsrc.NewDurationFlag( - &cli.DurationFlag{ - Name: "keepalive-interval", - Aliases: []string{"k"}, - EnvVars: []string{"KEEPALIVE_INTERVAL"}, - Value: 45 * time.Second, - Usage: "interval of keepalive messages", - }, - ), - } - - cmdServe := &cli.Command{ - Name: "serve", - Usage: "Run the server", - UsageText: "serve [OPTIONS..]", - Action: execServe, - Flags: flagsServe, - Before: initConfigFileInputSource("config", flagsServe), - } - - c := &cli.App{ - Name: "cmd", - HideVersion: true, - UseShortOptionHandling: true, - Commands: []*cli.Command{ - cmdServe, - }, - } - - if err := c.Run([]string{"cmd", "serve", "--config", "testdata/empty.yml"}); err != nil { - log.Fatal(err) - } - - // Output: - // keepalive 45s -} diff --git a/yaml_value_source.go b/yaml_value_source.go new file mode 100644 index 0000000..3736915 --- /dev/null +++ b/yaml_value_source.go @@ -0,0 +1,66 @@ +package altsrc + +import ( + "fmt" + + "github.com/urfave/cli/v3" + "gopkg.in/yaml.v3" +) + +// YAML is a helper function to encapsulate a number of +// yamlValueSource together as a cli.ValueSourceChain +func YAML(key string, paths ...string) cli.ValueSourceChain { + vsc := cli.ValueSourceChain{Chain: []cli.ValueSource{}} + + for _, path := range paths { + vsc.Chain = append( + vsc.Chain, + &yamlValueSource{ + file: path, + key: key, + maafsc: mapAnyAnyFileSourceCache{ + file: path, + f: yamlUnmarshalFile, + }, + }, + ) + } + + return vsc +} + +type yamlValueSource struct { + file string + key string + + maafsc mapAnyAnyFileSourceCache +} + +func (yvs *yamlValueSource) Lookup() (string, bool) { + if v, ok := nestedVal(yvs.key, yvs.maafsc.Get()); ok { + return fmt.Sprintf("%[1]v", v), ok + } + + return "", false +} + +func (yvs *yamlValueSource) String() string { + return fmt.Sprintf("yaml file %[1]q at key %[2]q", yvs.file, yvs.key) +} + +func (yvs *yamlValueSource) GoString() string { + return fmt.Sprintf("&yamlValueSource{file:%[1]q,keyPath:%[2]q}", yvs.file, yvs.key) +} + +func yamlUnmarshalFile(filePath string, container any) error { + b, err := readURI(filePath) + if err != nil { + return err + } + + if err := yaml.Unmarshal(b, container); err != nil { + return err + } + + return nil +} diff --git a/yaml_value_source_test.go b/yaml_value_source_test.go new file mode 100644 index 0000000..cdc94c0 --- /dev/null +++ b/yaml_value_source_test.go @@ -0,0 +1,29 @@ +package altsrc + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestYAML(t *testing.T) { + r := require.New(t) + + configPath := filepath.Join(testdataDir, "test_config.yaml") + altConfigPath := filepath.Join(testdataDir, "test_alt_config.yaml") + + vsc := YAML( + "water_fountain.water", + "/dev/null/nonexistent.yaml", + configPath, + altConfigPath, + ) + v, ok := vsc.Lookup() + r.Equal("false", v) + r.True(ok) + + yvs := vsc.Chain[0].(*yamlValueSource) + r.Equal("yaml file \"/dev/null/nonexistent.yaml\" at key \"water_fountain.water\"", yvs.String()) + r.Equal("&yamlValueSource{file:\"/dev/null/nonexistent.yaml\",keyPath:\"water_fountain.water\"}", yvs.GoString()) +}