From fcdccd4766ee6b51bd8dc1712a0a5d4b003c09b4 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Tue, 13 Jun 2023 09:22:24 -0400 Subject: [PATCH 01/10] Update dependencies (probably busted) --- .github/workflows/main.yml | 4 ++-- go.mod | 4 ++-- go.sum | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6a6e8b5..e13e61e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,10 @@ 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 diff --git a/go.mod b/go.mod index 5cd5402..ce54054 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ 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/urfave/cli/v3 v3.0.0-alpha3 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 0008b0e..998ff26 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ -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/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/urfave/cli/v3 v3.0.0-alpha3 h1:X5l0kjXlvKe3ExllWbcMisze3jDQl98/URz/6uGF3V4= +github.com/urfave/cli/v3 v3.0.0-alpha3/go.mod h1:gHI/xEYplFhOa3Y90xJleh3kqqsSanBj/19hVFxiVZ4= 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= From dd3a00288b5adcdbf793dece50550ffb5f2d0eec Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 22 Jun 2023 09:13:21 -0400 Subject: [PATCH 02/10] (Re)-integrating with urfave/cli/v3 ValueSource API --- altsrc.go | 40 +++++ examples_test.go | 88 ++++++++++ flag.go | 116 ++++++++----- flag_generated.go | 112 +++++-------- flag_test.go | 338 ++++++++++++++++++++++++++++++-------- go.mod | 7 +- go.sum | 8 + input_source_context.go | 7 +- json_command_test.go | 5 +- json_source_context.go | 81 ++++++++- map_input_source.go | 204 ++++++++++++++++++++--- map_input_source_test.go | 8 + toml_command_test.go | 21 +-- toml_file_loader.go | 2 +- yaml_command_test.go | 308 ---------------------------------- yaml_file_loader.go | 89 ---------- yaml_file_loader_test.go | 87 ---------- yaml_value_source.go | 125 ++++++++++++++ yaml_value_source_test.go | 42 +++++ 19 files changed, 971 insertions(+), 717 deletions(-) create mode 100644 altsrc.go create mode 100644 examples_test.go delete mode 100644 yaml_command_test.go delete mode 100644 yaml_file_loader.go delete mode 100644 yaml_file_loader_test.go create mode 100644 yaml_value_source.go create mode 100644 yaml_value_source_test.go diff --git a/altsrc.go b/altsrc.go new file mode 100644 index 0000000..299999d --- /dev/null +++ b/altsrc.go @@ -0,0 +1,40 @@ +package altsrc + +import ( + "fmt" + "os" + "runtime" + "strings" +) + +var ( + 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..., + ) +} diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..ff389ef --- /dev/null +++ b/examples_test.go @@ -0,0 +1,88 @@ +package altsrc_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + + altsrc "github.com/urfave/cli-altsrc" + "github.com/urfave/cli/v3" +) + +var ( + tmpDir string +) + +func init() { + if err := setupYAMLValueSourceExamples(); err != nil { + panic(err) + } +} + +func setupYAMLValueSourceExamples() error { + tmpDir = filepath.Join(os.TempDir(), "urfave-cli-altsrc-examples") + if err := os.MkdirAll(tmpDir, 0755); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(tmpDir, "config.yaml"), []byte(` +greet: + enthusiasm: 9001 +`), 0644); err != nil { + return err + } + + if err := os.WriteFile(filepath.Join(tmpDir, "alt-config.yaml"), []byte(` +greet: + name: Berry + enthusiasm: eleven +`), 0644); err != nil { + return err + } + + return nil +} + +func ExampleYAMLValueSource() { + configFiles := []string{ + filepath.Join(tmpDir, "config.yaml"), + filepath.Join(tmpDir, "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! +} diff --git a/flag.go b/flag.go index 50ebd9e..f620201 100644 --- a/flag.go +++ b/flag.go @@ -1,8 +1,8 @@ package altsrc +/* import ( "fmt" - "path/filepath" "strconv" "syscall" @@ -62,47 +62,55 @@ func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(cCtx *c } } -// ApplyInputSourceValue applies a generic value to the flagSet if required -func (f *GenericFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { +// 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.GenericFlag.Names() { + for _, name := range f.StringSliceFlag.Names() { if !isc.isSet(name) { continue } - value, err := isc.Generic(name) + value, err := isc.StringSlice(name) if err != nil { return err } if value == nil { continue } + var sliceValue = *(cli.NewStringSlice(value...)) for _, n := range f.Names() { - _ = f.set.Set(n, value.String()) + underlyingFlag := f.set.Lookup(n) + if underlyingFlag == nil { + continue + } + underlyingFlag.Value = &sliceValue + f.set.Set(n, sliceValue.Serialize()) + } + if f.Destination != nil { + f.Destination.Set(sliceValue.Serialize()) } } - return nil } -// ApplyInputSourceValue applies a StringSlice value to the flagSet if required -func (f *StringSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { +// 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.StringSliceFlag.Names() { + for _, name := range f.IntSliceFlag.Names() { if !isc.isSet(name) { continue } - value, err := isc.StringSlice(name) + value, err := isc.IntSlice(name) if err != nil { return err } if value == nil { continue } - var sliceValue = *(cli.NewStringSlice(value...)) + var sliceValue = *(cli.NewIntSlice(value...)) for _, n := range f.Names() { underlyingFlag := f.set.Lookup(n) if underlyingFlag == nil { @@ -117,23 +125,23 @@ func (f *StringSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSour return nil } -// ApplyInputSourceValue applies a IntSlice value if required -func (f *IntSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { +// 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.IntSliceFlag.Names() { + for _, name := range f.Int64SliceFlag.Names() { if !isc.isSet(name) { continue } - value, err := isc.IntSlice(name) + value, err := isc.Int64Slice(name) if err != nil { return err } if value == nil { continue } - var sliceValue = *(cli.NewIntSlice(value...)) + var sliceValue = *(cli.NewInt64Slice(value...)) for _, n := range f.Names() { underlyingFlag := f.set.Lookup(n) if underlyingFlag == nil { @@ -148,23 +156,23 @@ func (f *IntSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceC return nil } -// ApplyInputSourceValue applies a Int64Slice value if required -func (f *Int64SliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { +// ApplyInputSourceValue applies a Float64Slice value if required +func (f *Float64SliceFlag) 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() { + for _, name := range f.Float64SliceFlag.Names() { if !isc.isSet(name) { continue } - value, err := isc.Int64Slice(name) + value, err := isc.Float64Slice(name) if err != nil { return err } if value == nil { continue } - var sliceValue = *(cli.NewInt64Slice(value...)) + var sliceValue = *(cli.NewFloat64Slice(value...)) for _, n := range f.Names() { underlyingFlag := f.set.Lookup(n) if underlyingFlag == nil { @@ -219,51 +227,78 @@ func (f *StringFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceCon return nil } -// ApplyInputSourceValue applies a Path value to the flagSet if required -func (f *PathFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { +// 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.PathFlag.Names() { + for _, name := range f.IntFlag.Names() { if !isc.isSet(name) { continue } - value, err := isc.String(name) + value, err := isc.Int(name) if err != nil { return err } - if value == "" { + for _, n := range f.Names() { + _ = f.set.Set(n, strconv.FormatInt(int64(value), 10)) + } + } + return nil +} + +func (f *Int64Flag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.Int64Flag.Names() { + if !isc.isSet(name) { continue } + value, err := isc.Int64(name) + if err != nil { + return err + } 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) + _ = f.set.Set(n, strconv.FormatInt(value, 10)) } } return nil } -// ApplyInputSourceValue applies a int value to the flagSet if required -func (f *IntFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { +func (f *UintFlag) 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() { + for _, name := range f.UintFlag.Names() { if !isc.isSet(name) { continue } - value, err := isc.Int(name) + value, err := isc.Uint(name) if err != nil { return err } for _, n := range f.Names() { - _ = f.set.Set(n, strconv.FormatInt(int64(value), 10)) + _ = f.set.Set(n, strconv.FormatUint(uint64(value), 10)) + } + } + return nil +} + +func (f *Uint64Flag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.Uint64Flag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.Uint64(name) + if err != nil { + return err + } + for _, n := range f.Names() { + _ = f.set.Set(n, strconv.FormatUint(value, 10)) } } return nil @@ -326,3 +361,4 @@ func isEnvVarSet(envVars []string) bool { func float64ToString(f float64) string { return fmt.Sprintf("%v", f) } +*/ diff --git a/flag_generated.go b/flag_generated.go index 2a521d1..2959de6 100644 --- a/flag_generated.go +++ b/flag_generated.go @@ -84,25 +84,6 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { 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 { @@ -122,43 +103,43 @@ func (f *IntFlag) Apply(set *flag.FlagSet) error { 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) -} +// // 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 @@ -179,25 +160,6 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { 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 { diff --git a/flag_test.go b/flag_test.go index 7f85d74..d45839c 100644 --- a/flag_test.go +++ b/flag_test.go @@ -1,5 +1,6 @@ package altsrc +/* import ( "flag" "fmt" @@ -37,89 +38,29 @@ func (ris *racyInputSource) isSet(name string) bool { 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() + destValue := dest.Value() tis := testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), + Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: &destValue}), 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"}) + expect(t, destValue, []string{"hello", "world"}) // reset dest dest = cli.NewStringSlice() + destValue = dest.Value() tis = testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), + Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: &destValue}), 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"}) + refute(t, destValue, []string{"hello", "world"}) } func TestStringSliceApplyInputSourceValue(t *testing.T) { @@ -280,6 +221,65 @@ func TestInt64SliceFlagApplyInputSourceValue(t *testing.T) { refute(t, dest.Value(), []int64{1, 2}) } +func TestInt64SliceFlagApplyInputSourceValueNotSet(t *testing.T) { + dest := cli.NewInt64Slice() + tis := testApplyInputSource{ + Flag: NewInt64SliceFlag(&cli.Int64SliceFlag{Name: "test", Destination: dest}), + FlagName: "test1", + MapValue: []interface{}{int64(1), int64(2)}, + } + c := runTest(t, tis) + expect(t, c.Int64Slice("test"), []int64{}) + expect(t, dest.Value(), []int64{}) +} + +func TestFloat64SliceFlagApplyInputSourceValue(t *testing.T) { + dest := cli.NewFloat64Slice() + tis := testApplyInputSource{ + Flag: NewFloat64SliceFlag(&cli.Float64SliceFlag{Name: "test", Destination: dest}), + FlagName: "test", + MapValue: []interface{}{float64(1.0), float64(2.1)}, + } + c := runTest(t, tis) + expect(t, c.Float64Slice("test"), []float64{1.0, 2.1}) + expect(t, dest.Value(), []float64{1.0, 2.1}) + + // reset dest + dest = cli.NewFloat64Slice() + tis = testApplyInputSource{ + Flag: NewFloat64SliceFlag(&cli.Float64SliceFlag{Name: "test", Destination: dest}), + FlagName: "test", + MapValue: []interface{}{float64(1.0), float64(2.1)}, + } + c = runRacyTest(t, tis) + refute(t, c.IntSlice("test"), []int64{1, 2}) + refute(t, dest.Value(), []int64{1, 2}) +} + +func TestFloat64SliceFlagApplyInputSourceValueNotSet(t *testing.T) { + dest := cli.NewFloat64Slice() + tis := testApplyInputSource{ + Flag: NewFloat64SliceFlag(&cli.Float64SliceFlag{Name: "test", Destination: dest}), + FlagName: "test1", + MapValue: []interface{}{float64(1.0), float64(2.1)}, + } + c := runTest(t, tis) + expect(t, c.Float64Slice("test"), []float64{}) + expect(t, dest.Value(), []float64{}) +} + +func TestFloat64SliceFlagApplyInputSourceValueInvalidType(t *testing.T) { + dest := cli.NewFloat64Slice() + tis := testApplyInputSource{ + Flag: NewFloat64SliceFlag(&cli.Float64SliceFlag{Name: "test", Destination: dest}), + FlagName: "test", + MapValue: []interface{}{1, 2}, + } + c := runTest(t, tis) + expect(t, c.Float64Slice("test"), []float64{}) + expect(t, dest.Value(), []float64{}) +} + func TestBoolApplyInputSourceMethodSet(t *testing.T) { tis := testApplyInputSource{ Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}), @@ -524,6 +524,27 @@ func TestIntApplyInputSourceMethodContextSet(t *testing.T) { refute(t, 7, c.Int("test")) } +func TestIntApplyInputSourceMethodContextNotSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), + FlagName: "test1", + MapValue: 15, + ContextValueString: "7", + } + c := runTest(t, tis) + expect(t, 0, c.Int("test")) +} + +func TestIntApplyInputSourceMethodContextSetInvalidType(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), + FlagName: "test", + ContextValueString: "d", + } + c := runTest(t, tis) + expect(t, 0, c.Int("test")) +} + func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) { tis := testApplyInputSource{ Flag: NewIntFlag(&cli.IntFlag{Name: "test", EnvVars: []string{"TEST"}}), @@ -539,6 +560,190 @@ func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) { refute(t, 12, c.Int("test")) } +func TestInt64ApplyInputSourceMethodSet_Alias(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewInt64Flag(&cli.Int64Flag{Name: "test", Aliases: []string{"test_alias"}}), + FlagName: "test_alias", + MapValue: int64(15), + } + c := runTest(t, tis) + expect(t, int64(15), c.Int64("test_alias")) + + c = runRacyTest(t, tis) + refute(t, int64(15), c.Int64("test_alias")) +} + +func TestInt64ApplyInputSourceMethodSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), + FlagName: "test", + MapValue: int64(15), + } + c := runTest(t, tis) + expect(t, int64(15), c.Int64("test")) + + c = runRacyTest(t, tis) + refute(t, int64(15), c.Int("test")) +} + +func TestInt64ApplyInputSourceMethodSetNegativeValue(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), + FlagName: "test", + MapValue: int64(-1), + } + c := runTest(t, tis) + expect(t, int64(-1), c.Int64("test")) + + c = runRacyTest(t, tis) + refute(t, int64(-1), c.Int("test")) +} + +func TestInt64ApplyInputSourceMethodContextSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), + FlagName: "test", + MapValue: 15, + ContextValueString: "7", + } + c := runTest(t, tis) + expect(t, int64(7), c.Int64("test")) + + c = runRacyTest(t, tis) + refute(t, int64(7), c.Int64("test")) +} + +func TestInt64ApplyInputSourceMethodContextNotSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), + FlagName: "test1", + MapValue: 15, + ContextValueString: "7", + } + c := runTest(t, tis) + expect(t, int64(0), c.Int64("test")) +} + +func TestInt64ApplyInputSourceMethodContextSetInvalidType(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), + FlagName: "test", + ContextValueString: "d", + } + c := runTest(t, tis) + expect(t, int64(0), c.Int64("test")) +} + +func TestInt64ApplyInputSourceMethodEnvVarSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewInt64Flag(&cli.Int64Flag{Name: "test", EnvVars: []string{"TEST"}}), + FlagName: "test", + MapValue: 15, + EnvVarName: "TEST", + EnvVarValue: "12", + } + c := runTest(t, tis) + expect(t, int64(12), c.Int64("test")) + + c = runRacyTest(t, tis) + refute(t, int64(12), c.Int64("test")) +} + +func TestUintApplyInputSourceMethodSet_Alias(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewUintFlag(&cli.UintFlag{Name: "test", Aliases: []string{"test_alias"}}), + FlagName: "test_alias", + MapValue: uint(15), + } + c := runTest(t, tis) + expect(t, uint(15), c.Uint("test_alias")) + + c = runRacyTest(t, tis) + refute(t, uint(15), c.Uint("test_alias")) +} + +func TestUintApplyInputSourceMethodSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewUintFlag(&cli.UintFlag{Name: "test"}), + FlagName: "test", + MapValue: uint(15), + } + c := runTest(t, tis) + expect(t, uint(15), c.Uint("test")) + + c = runRacyTest(t, tis) + refute(t, uint(15), c.Uint("test")) +} + +func TestUintApplyInputSourceMethodContextSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewUintFlag(&cli.UintFlag{Name: "test"}), + FlagName: "test", + MapValue: uint(15), + ContextValueString: "7", + } + c := runTest(t, tis) + expect(t, uint(7), c.Uint("test")) + + c = runRacyTest(t, tis) + refute(t, uint(7), c.Uint("test")) +} + +func TestUint64ApplyInputSourceMethodSet_Alias(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewUint64Flag(&cli.Uint64Flag{Name: "test", Aliases: []string{"test_alias"}}), + FlagName: "test_alias", + MapValue: uint64(15), + } + c := runTest(t, tis) + expect(t, uint64(15), c.Uint64("test_alias")) + + c = runRacyTest(t, tis) + refute(t, uint64(15), c.Uint64("test_alias")) +} + +func TestUint64ApplyInputSourceMethodSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewUint64Flag(&cli.Uint64Flag{Name: "test"}), + FlagName: "test", + MapValue: uint64(15), + } + c := runTest(t, tis) + expect(t, uint64(15), c.Uint64("test")) + + c = runRacyTest(t, tis) + refute(t, uint64(15), c.Uint64("test")) +} + +func TestUint64ApplyInputSourceMethodContextSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewUint64Flag(&cli.Uint64Flag{Name: "test"}), + FlagName: "test", + MapValue: uint64(15), + ContextValueString: "7", + } + c := runTest(t, tis) + expect(t, uint64(7), c.Uint64("test")) + + c = runRacyTest(t, tis) + refute(t, uint64(7), c.Uint64("test")) +} + +func TestUint64ApplyInputSourceMethodEnvVarSet(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewUint64Flag(&cli.Uint64Flag{Name: "test", EnvVars: []string{"TEST"}}), + FlagName: "test", + MapValue: uint64(15), + EnvVarName: "TEST", + EnvVarValue: "12", + } + c := runTest(t, tis) + expect(t, uint64(12), c.Uint64("test")) + + c = runRacyTest(t, tis) + refute(t, uint64(12), c.Uint64("test")) +} + func TestDurationApplyInputSourceMethodSet_Alias(t *testing.T) { tis := testApplyInputSource{ Flag: NewDurationFlag(&cli.DurationFlag{Name: "test", Aliases: []string{"test_alias"}}), @@ -741,3 +946,4 @@ func (p *Parser) String() string { } type bogus [1]uint +*/ diff --git a/go.mod b/go.mod index ce54054..c3d3a5e 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,18 @@ -module github.com/urfave/cli-altsrc/v3 +module github.com/urfave/cli-altsrc go 1.18 require ( github.com/BurntSushi/toml v1.3.2 - github.com/urfave/cli/v3 v3.0.0-alpha3 + 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 998ff26..03f1ef0 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,18 @@ 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/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-alpha3 h1:X5l0kjXlvKe3ExllWbcMisze3jDQl98/URz/6uGF3V4= github.com/urfave/cli/v3 v3.0.0-alpha3/go.mod h1:gHI/xEYplFhOa3Y90xJleh3kqqsSanBj/19hVFxiVZ4= +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/input_source_context.go b/input_source_context.go index 4cd8392..2ca6055 100644 --- a/input_source_context.go +++ b/input_source_context.go @@ -2,8 +2,6 @@ package altsrc import ( "time" - - "github.com/urfave/cli/v3" ) // InputSourceContext is an interface used to allow @@ -15,13 +13,16 @@ type InputSourceContext interface { Source() string Int(name string) (int, error) + Int64(name string) (int64, error) + Uint(name string) (uint, error) + Uint64(name string) (uint64, 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) + Float64Slice(name string) ([]float64, error) Bool(name string) (bool, error) isSet(name string) bool diff --git a/json_command_test.go b/json_command_test.go index e39dabc..f0a10e8 100644 --- a/json_command_test.go +++ b/json_command_test.go @@ -1,8 +1,8 @@ package altsrc +/* import ( "flag" - "io/ioutil" "os" "testing" @@ -318,7 +318,7 @@ func TestCommandJSONFileFlagHasDefaultGlobalEnvJSONSetGlobalEnvWinsNested(t *tes } func writeTempFile(t *testing.T, name string, content string) func() { - if err := ioutil.WriteFile(name, []byte(content), 0666); err != nil { + if err := os.WriteFile(name, []byte(content), 0666); err != nil { t.Fatalf("cannot write %q: %v", name, err) } return func() { @@ -327,3 +327,4 @@ func writeTempFile(t *testing.T, name string, content string) func() { } } } +*/ diff --git a/json_source_context.go b/json_source_context.go index 5589d59..88c57c9 100644 --- a/json_source_context.go +++ b/json_source_context.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "strings" "time" @@ -29,7 +28,7 @@ func NewJSONSourceFromFlagFunc(flag string) func(c *cli.Context) (InputSourceCon // retrieving config variables from a file (or url) containing JSON // data. func NewJSONSourceFromFile(f string) (InputSourceContext, error) { - data, err := loadDataFrom(f) + data, err := readURI(f) if err != nil { return nil, err } @@ -40,7 +39,7 @@ func NewJSONSourceFromFile(f string) (InputSourceContext, error) { // 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) + data, err := io.ReadAll(r) if err != nil { return nil, err } @@ -78,6 +77,63 @@ func (x *jsonSource) Int(name string) (int, error) { } } +func (x *jsonSource) Int64(name string) (int64, 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 int64: + return v, nil + case int: + return int64(v), nil + case float32: + return int64(v), nil + case float64: + return int64(v), nil + } +} + +func (x *jsonSource) Uint(name string) (uint, 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 uint: + return v, nil + case uint64: + return uint(v), nil + case float32: + return uint(v), nil + case float64: + return uint(v), nil + } +} + +func (x *jsonSource) Uint64(name string) (uint64, 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 uint64: + return v, nil + case uint: + return uint64(v), nil + case float32: + return uint64(v), nil + case float64: + return uint64(v), nil + } +} + func (x *jsonSource) Duration(name string) (time.Duration, error) { i, err := x.getValue(name) if err != nil { @@ -183,16 +239,27 @@ func (x *jsonSource) Int64Slice(name string) ([]int64, error) { } } -func (x *jsonSource) Generic(name string) (cli.Generic, error) { +func (x *jsonSource) Float64Slice(name string) ([]float64, error) { i, err := x.getValue(name) if err != nil { return nil, err } - v, ok := i.(cli.Generic) - if !ok { + switch v := i.(type) { + default: return nil, fmt.Errorf("unexpected type %T for %q", i, name) + case []float64: + return v, nil + case []interface{}: + c := []float64{} + for _, s := range v { + if i2, ok := s.(float64); ok { + c = append(c, i2) + } else { + return c, fmt.Errorf("unexpected item type %T in %T for %q", s, c, name) + } + } + return c, nil } - return v, nil } func (x *jsonSource) Bool(name string) (bool, error) { diff --git a/map_input_source.go b/map_input_source.go index 4bac92c..cc3990d 100644 --- a/map_input_source.go +++ b/map_input_source.go @@ -2,11 +2,9 @@ package altsrc import ( "fmt" - "reflect" + "math" "strings" "time" - - "github.com/urfave/cli/v3" ) // MapInputSource implements InputSourceContext to return @@ -127,6 +125,159 @@ func (fsm *MapInputSource) Float64(name string) (float64, error) { return 0, nil } +func castToInt64(v interface{}) (int64, bool) { + int64Value := int64(0) + isType := false + if v == nil { + return int64Value, true + } + + // There are only four cases(int, int64, uint64, float64) when parsing the integer in yaml.v3 pkg + // But the cases, uint64, float64, are an error case so that ignored + switch value := v.(type) { + case int: + int64Value = int64(value) + isType = true + case int64: + int64Value = int64(value) + isType = true + } + return int64Value, isType +} + +// Int64 returns an int64 from the map if it exists otherwise returns 0 +func (fsm *MapInputSource) Int64(name string) (int64, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := castToInt64(otherGenericValue) + if !isType { + return 0, incorrectTypeForFlagError(name, "int64", otherGenericValue) + } + return otherValue, nil + } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := castToInt64(otherGenericValue) + if !isType { + return 0, incorrectTypeForFlagError(name, "int64", nestedGenericValue) + } + return otherValue, nil + } + + return 0, nil +} + +func castToUint(v interface{}) (uint, bool) { + uintValue := uint(0) + isType := false + if v == nil { + return uintValue, true + } + + // There are only four cases(int, int64, uint64, float64) when parsing the integer in yaml.v3 pkg + // But the last case, float64, is an error case so that ignored + switch value := v.(type) { + case int: + intValue := int(value) + if intValue >= 0 { + uintValue = uint(value) + isType = true + } + case int64: + int64Value := int64(value) + if int64Value >= 0 && uint64(int64Value) <= math.MaxUint { + uintValue = uint(value) + isType = true + } + case uint: + uintValue = uint(value) + isType = true + case uint64: + uint64Value := uint64(value) + if uint64Value <= math.MaxUint { + uintValue = uint(value) + isType = true + } + } + return uintValue, isType +} + +// Int64 returns an int64 from the map if it exists otherwise returns 0 +func (fsm *MapInputSource) Uint(name string) (uint, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := castToUint(otherGenericValue) + if !isType { + return 0, incorrectTypeForFlagError(name, "uint", otherGenericValue) + } + return otherValue, nil + } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := castToUint(nestedGenericValue) + if !isType { + return 0, incorrectTypeForFlagError(name, "uint", nestedGenericValue) + } + return otherValue, nil + } + + return 0, nil +} + +func castToUint64(v interface{}) (uint64, bool) { + uint64Value := uint64(0) + isType := false + if v == nil { + return uint64Value, true + } + + // There are only four cases(int, int64, uint64, float64) when parsing the integer in yaml.v3 pkg + // But the last case, float64, is an error case so that ignored + switch value := v.(type) { + case int: + intValue := int(value) + if intValue >= 0 { + uint64Value = uint64(intValue) + isType = true + } + case int64: + int64Value := int64(value) + if int64Value >= 0 { + uint64Value = uint64(int64Value) + isType = true + } + case uint: + uint64Value = uint64(value) + isType = true + case uint64: + uint64Value = uint64(value) + isType = true + } + return uint64Value, isType +} + +// UInt64 returns an uint64 from the map if it exists otherwise returns 0 +func (fsm *MapInputSource) Uint64(name string) (uint64, error) { + otherGenericValue, exists := fsm.valueMap[name] + if exists { + otherValue, isType := castToUint64(otherGenericValue) + if !isType { + return 0, incorrectTypeForFlagError(name, "uint64", otherGenericValue) + } + return otherValue, nil + } + nestedGenericValue, exists := nestedVal(name, fsm.valueMap) + if exists { + otherValue, isType := castToUint64(nestedGenericValue) + if !isType { + return 0, incorrectTypeForFlagError(name, "uint64", 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] @@ -224,38 +375,43 @@ func (fsm *MapInputSource) Int64Slice(name string) ([]int64, error) { var int64Slice = make([]int64, 0, len(otherValue)) for i, v := range otherValue { - int64Value, isType := v.(int64) - + int64Value, isType := castToInt64(v) if !isType { - return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v) + return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int64", 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) { +// Float64Slice returns an []float64 from the map if it exists otherwise returns nil +func (fsm *MapInputSource) Float64Slice(name string) ([]float64, error) { otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.(cli.Generic) - if !isType { - return nil, incorrectTypeForFlagError(name, "cli.Generic", otherGenericValue) + if !exists { + otherGenericValue, exists = nestedVal(name, fsm.valueMap) + if !exists { + return nil, nil } - return otherValue, nil } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.(cli.Generic) + + otherValue, isType := otherGenericValue.([]interface{}) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) + } + + var float64Slice = make([]float64, 0, len(otherValue)) + for i, v := range otherValue { + float64Value, isType := v.(float64) + if !isType { - return nil, incorrectTypeForFlagError(name, "cli.Generic", nestedGenericValue) + return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v) } - return otherValue, nil + + float64Slice = append(float64Slice, float64Value) } - return nil, nil + return float64Slice, nil } // Bool returns an bool from the map otherwise returns false @@ -290,11 +446,5 @@ func (fsm *MapInputSource) isSet(name string) bool { } 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) + return fmt.Errorf("Mismatched type for flag '%s'. Expected '%s' but actual is '%T'", name, expectedTypeName, value) } diff --git a/map_input_source_test.go b/map_input_source_test.go index cf399b5..3ba20c7 100644 --- a/map_input_source_test.go +++ b/map_input_source_test.go @@ -1,6 +1,8 @@ package altsrc +/* import ( + "fmt" "testing" "time" ) @@ -33,3 +35,9 @@ func TestMapInputSource_Int64Slice(t *testing.T) { expect(t, []int64{1, 2, 3}, d) expect(t, nil, err) } + +func TestMapInputSource_IncorrectFlagTypeError(t *testing.T) { + var testVal *bool + expect(t, incorrectTypeForFlagError("test", "bool", testVal), fmt.Errorf("Mismatched type for flag 'test'. Expected 'bool' but actual is '*bool'")) +} +*/ diff --git a/toml_command_test.go b/toml_command_test.go index 9e52435..b123ac6 100644 --- a/toml_command_test.go +++ b/toml_command_test.go @@ -1,8 +1,8 @@ package altsrc +/* import ( "flag" - "io/ioutil" "os" "testing" @@ -12,7 +12,7 @@ import ( func TestCommandTomFileTest(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = os.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") test := []string{"test-cmd", "--load", "current.toml"} _ = set.Parse(test) @@ -42,7 +42,7 @@ func TestCommandTomFileTest(t *testing.T) { func TestCommandTomlFileTestGlobalEnvVarWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = os.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") _ = os.Setenv("THE_TEST", "10") @@ -76,7 +76,7 @@ func TestCommandTomlFileTestGlobalEnvVarWins(t *testing.T) { func TestCommandTomlFileTestGlobalEnvVarWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) + _ = os.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) defer os.Remove("current.toml") _ = os.Setenv("THE_TEST", "10") @@ -110,7 +110,7 @@ func TestCommandTomlFileTestGlobalEnvVarWinsNested(t *testing.T) { func TestCommandTomlFileTestSpecifiedFlagWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = os.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") test := []string{"test-cmd", "--load", "current.toml", "--test", "7"} @@ -142,7 +142,7 @@ func TestCommandTomlFileTestSpecifiedFlagWins(t *testing.T) { func TestCommandTomlFileTestSpecifiedFlagWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte(`[top] + _ = os.WriteFile("current.toml", []byte(`[top] test = 15`), 0666) defer os.Remove("current.toml") @@ -175,7 +175,7 @@ func TestCommandTomlFileTestSpecifiedFlagWinsNested(t *testing.T) { func TestCommandTomlFileTestDefaultValueFileWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = os.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") test := []string{"test-cmd", "--load", "current.toml"} @@ -207,7 +207,7 @@ func TestCommandTomlFileTestDefaultValueFileWins(t *testing.T) { func TestCommandTomlFileTestDefaultValueFileWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) + _ = os.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) defer os.Remove("current.toml") test := []string{"test-cmd", "--load", "current.toml"} @@ -239,7 +239,7 @@ func TestCommandTomlFileTestDefaultValueFileWinsNested(t *testing.T) { func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = os.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") _ = os.Setenv("THE_TEST", "11") @@ -273,7 +273,7 @@ func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWins(t *testing.T func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - _ = ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) + _ = os.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) defer os.Remove("current.toml") _ = os.Setenv("THE_TEST", "11") @@ -303,3 +303,4 @@ func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWinsNested(t *tes expect(t, err, nil) } +*/ diff --git a/toml_file_loader.go b/toml_file_loader.go index 0e614e3..7d4f3c3 100644 --- a/toml_file_loader.go +++ b/toml_file_loader.go @@ -97,7 +97,7 @@ func NewTomlSourceFromFlagFunc(flagFileName string) func(cCtx *cli.Context) (Inp } func readCommandToml(filePath string, container interface{}) (err error) { - b, err := loadDataFrom(filePath) + b, err := readURI(filePath) if err != nil { return err } 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..de0d2c6 --- /dev/null +++ b/yaml_value_source.go @@ -0,0 +1,125 @@ +package altsrc + +import ( + "fmt" + "io" + "net/http" + "net/url" + "os" + "runtime" + "strings" + + "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, + ymc: yamlMapInputSourceCache{file: path}, + }, + ) + } + + return vsc +} + +type yamlMapInputSourceCache struct { + file string + m *map[any]any +} + +func (ymc *yamlMapInputSourceCache) Get() map[any]any { + if ymc.m == nil { + res := map[any]any{} + if err := yamlUnmarshalFile(ymc.file, &res); err == nil { + ymc.m = &res + } else { + tracef("failed to unmarshal yaml from file %[1]q: %[2]v", ymc.file, err) + } + } + + if ymc.m == nil { + tracef("returning empty map") + return map[any]any{} + } + + return *ymc.m +} + +type yamlValueSource struct { + file string + key string + + ymc yamlMapInputSourceCache +} + +func (yvs *yamlValueSource) Lookup() (string, bool) { + if v, ok := nestedVal(yvs.key, yvs.ymc.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 +} + +func readURI(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 io.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 os.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 os.ReadFile(filePath) + } + + return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) +} diff --git a/yaml_value_source_test.go b/yaml_value_source_test.go new file mode 100644 index 0000000..311e808 --- /dev/null +++ b/yaml_value_source_test.go @@ -0,0 +1,42 @@ +package altsrc + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestYAML(t *testing.T) { + r := require.New(t) + + tmpDir := t.TempDir() + + configPath := filepath.Join(tmpDir, "config.yaml") + altConfigPath := filepath.Join(tmpDir, "alt-config.yaml") + + r.NoError(os.WriteFile(configPath, []byte(` +water_fountain: + water: false +woodstock: + wood: false +`), 0644)) + + r.NoError(os.WriteFile(altConfigPath, []byte(` +water_fountain: + water: true +phone_booth: + phone: false +`), 0644)) + + vsc := YAML( + "water_fountain.water", + "/dev/null/nonexistent.yaml", + configPath, + altConfigPath, + ) + v, ok := vsc.Lookup() + r.Equal("false", v) + r.True(ok) +} From caff2dc14a3ba715c7424bde119b94e4c0565855 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 22 Jun 2023 20:05:51 -0400 Subject: [PATCH 03/10] Add wrapper func for JSON value source --- yaml_value_source.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/yaml_value_source.go b/yaml_value_source.go index de0d2c6..58a1511 100644 --- a/yaml_value_source.go +++ b/yaml_value_source.go @@ -13,6 +13,12 @@ import ( "gopkg.in/yaml.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...) +} + // YAML is a helper function to encapsulate a number of // yamlValueSource together as a cli.ValueSourceChain func YAML(key string, paths ...string) cli.ValueSourceChain { From 8f10469380399a25a3535543503d6101f7fcabb0 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 22 Jun 2023 22:17:44 -0400 Subject: [PATCH 04/10] Implement `TOML` value source func and remove all the unused bits --- README.md | 2 +- altsrc.go | 67 +++ default_input_source.go | 6 - file_source_cache.go | 38 ++ flag-spec.yaml | 18 - flag.go | 364 --------------- flag_generated.go | 239 ---------- flag_test.go | 949 -------------------------------------- helpers_test.go | 31 -- input_source_context.go | 29 -- json_command_test.go | 330 ------------- json_source_context.go | 307 ------------ json_value_source.go | 9 + json_value_source_test.go | 50 ++ map_input_source.go | 450 ------------------ map_input_source_test.go | 43 -- toml_command_test.go | 306 ------------ toml_file_loader.go | 112 ----- toml_map.go | 69 +++ toml_value_source.go | 61 +++ toml_value_source_test.go | 44 ++ yaml_value_source.go | 78 +--- 22 files changed, 346 insertions(+), 3256 deletions(-) delete mode 100644 default_input_source.go create mode 100644 file_source_cache.go delete mode 100644 flag-spec.yaml delete mode 100644 flag.go delete mode 100644 flag_generated.go delete mode 100644 flag_test.go delete mode 100644 helpers_test.go delete mode 100644 input_source_context.go delete mode 100644 json_command_test.go delete mode 100644 json_source_context.go create mode 100644 json_value_source.go create mode 100644 json_value_source_test.go delete mode 100644 map_input_source.go delete mode 100644 map_input_source_test.go delete mode 100644 toml_command_test.go delete mode 100644 toml_file_loader.go create mode 100644 toml_map.go create mode 100644 toml_value_source.go create mode 100644 toml_value_source_test.go diff --git a/README.md b/README.md index fd2840e..7d5f62f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # cli-altsrc -Configuration source integration library for urfave/cli +Configuration source integration library for urfave/cli/v3 diff --git a/altsrc.go b/altsrc.go index 299999d..0d785d1 100644 --- a/altsrc.go +++ b/altsrc.go @@ -2,6 +2,9 @@ package altsrc import ( "fmt" + "io" + "net/http" + "net/url" "os" "runtime" "strings" @@ -38,3 +41,67 @@ func tracef(format string, a ...any) { a..., ) } + +func readURI(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 io.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 os.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 os.ReadFile(filePath) + } + + return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) +} + +// 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 +} 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/file_source_cache.go b/file_source_cache.go new file mode 100644 index 0000000..cc136e2 --- /dev/null +++ b/file_source_cache.go @@ -0,0 +1,38 @@ +package altsrc + +type fileSourceCache[T any] struct { + file string + m *T + nf func() T + f func(string, any) error +} + +func (fsc *fileSourceCache[T]) Get() T { + if fsc.m == nil { + res := fsc.nf() + 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 fsc.nf() + } + + return *fsc.m +} + +func newMapAnyAny() map[any]any { + return map[any]any{} +} + +type mapAnyAnyFileSourceCache = fileSourceCache[map[any]any] + +func newTomlMap() tomlMap { + return tomlMap{Map: 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 f620201..0000000 --- a/flag.go +++ /dev/null @@ -1,364 +0,0 @@ -package altsrc - -/* -import ( - "fmt" - "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 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 - f.set.Set(n, sliceValue.Serialize()) - } - 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 Float64Slice value if required -func (f *Float64SliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.Float64SliceFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Float64Slice(name) - if err != nil { - return err - } - if value == nil { - continue - } - var sliceValue = *(cli.NewFloat64Slice(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 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 -} - -func (f *Int64Flag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.Int64Flag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Int64(name) - if err != nil { - return err - } - for _, n := range f.Names() { - _ = f.set.Set(n, strconv.FormatInt(value, 10)) - } - } - return nil -} - -func (f *UintFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.UintFlag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Uint(name) - if err != nil { - return err - } - for _, n := range f.Names() { - _ = f.set.Set(n, strconv.FormatUint(uint64(value), 10)) - } - } - return nil -} - -func (f *Uint64Flag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { - return nil - } - for _, name := range f.Uint64Flag.Names() { - if !isc.isSet(name) { - continue - } - value, err := isc.Uint64(name) - if err != nil { - return err - } - for _, n := range f.Names() { - _ = f.set.Set(n, strconv.FormatUint(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 2959de6..0000000 --- a/flag_generated.go +++ /dev/null @@ -1,239 +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) -} - -// 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) -} - -// 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 d45839c..0000000 --- a/flag_test.go +++ /dev/null @@ -1,949 +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 TestStringSliceApplyInputSourceValue_Alias(t *testing.T) { - dest := cli.NewStringSlice() - destValue := dest.Value() - tis := testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: &destValue}), - FlagName: "test_alias", - MapValue: []interface{}{"hello", "world"}, - } - c := runTest(t, tis) - expect(t, c.StringSlice("test_alias"), []string{"hello", "world"}) - expect(t, destValue, []string{"hello", "world"}) - - // reset dest - dest = cli.NewStringSlice() - destValue = dest.Value() - tis = testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: &destValue}), - FlagName: "test_alias", - MapValue: []interface{}{"hello", "world"}, - } - c = runRacyTest(t, tis) - refute(t, c.StringSlice("test_alias"), []string{"hello", "world"}) - refute(t, destValue, []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 TestInt64SliceFlagApplyInputSourceValueNotSet(t *testing.T) { - dest := cli.NewInt64Slice() - tis := testApplyInputSource{ - Flag: NewInt64SliceFlag(&cli.Int64SliceFlag{Name: "test", Destination: dest}), - FlagName: "test1", - MapValue: []interface{}{int64(1), int64(2)}, - } - c := runTest(t, tis) - expect(t, c.Int64Slice("test"), []int64{}) - expect(t, dest.Value(), []int64{}) -} - -func TestFloat64SliceFlagApplyInputSourceValue(t *testing.T) { - dest := cli.NewFloat64Slice() - tis := testApplyInputSource{ - Flag: NewFloat64SliceFlag(&cli.Float64SliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{float64(1.0), float64(2.1)}, - } - c := runTest(t, tis) - expect(t, c.Float64Slice("test"), []float64{1.0, 2.1}) - expect(t, dest.Value(), []float64{1.0, 2.1}) - - // reset dest - dest = cli.NewFloat64Slice() - tis = testApplyInputSource{ - Flag: NewFloat64SliceFlag(&cli.Float64SliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{float64(1.0), float64(2.1)}, - } - c = runRacyTest(t, tis) - refute(t, c.IntSlice("test"), []int64{1, 2}) - refute(t, dest.Value(), []int64{1, 2}) -} - -func TestFloat64SliceFlagApplyInputSourceValueNotSet(t *testing.T) { - dest := cli.NewFloat64Slice() - tis := testApplyInputSource{ - Flag: NewFloat64SliceFlag(&cli.Float64SliceFlag{Name: "test", Destination: dest}), - FlagName: "test1", - MapValue: []interface{}{float64(1.0), float64(2.1)}, - } - c := runTest(t, tis) - expect(t, c.Float64Slice("test"), []float64{}) - expect(t, dest.Value(), []float64{}) -} - -func TestFloat64SliceFlagApplyInputSourceValueInvalidType(t *testing.T) { - dest := cli.NewFloat64Slice() - tis := testApplyInputSource{ - Flag: NewFloat64SliceFlag(&cli.Float64SliceFlag{Name: "test", Destination: dest}), - FlagName: "test", - MapValue: []interface{}{1, 2}, - } - c := runTest(t, tis) - expect(t, c.Float64Slice("test"), []float64{}) - expect(t, dest.Value(), []float64{}) -} - -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 TestIntApplyInputSourceMethodContextNotSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), - FlagName: "test1", - MapValue: 15, - ContextValueString: "7", - } - c := runTest(t, tis) - expect(t, 0, c.Int("test")) -} - -func TestIntApplyInputSourceMethodContextSetInvalidType(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), - FlagName: "test", - ContextValueString: "d", - } - c := runTest(t, tis) - expect(t, 0, 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 TestInt64ApplyInputSourceMethodSet_Alias(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewInt64Flag(&cli.Int64Flag{Name: "test", Aliases: []string{"test_alias"}}), - FlagName: "test_alias", - MapValue: int64(15), - } - c := runTest(t, tis) - expect(t, int64(15), c.Int64("test_alias")) - - c = runRacyTest(t, tis) - refute(t, int64(15), c.Int64("test_alias")) -} - -func TestInt64ApplyInputSourceMethodSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), - FlagName: "test", - MapValue: int64(15), - } - c := runTest(t, tis) - expect(t, int64(15), c.Int64("test")) - - c = runRacyTest(t, tis) - refute(t, int64(15), c.Int("test")) -} - -func TestInt64ApplyInputSourceMethodSetNegativeValue(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), - FlagName: "test", - MapValue: int64(-1), - } - c := runTest(t, tis) - expect(t, int64(-1), c.Int64("test")) - - c = runRacyTest(t, tis) - refute(t, int64(-1), c.Int("test")) -} - -func TestInt64ApplyInputSourceMethodContextSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), - FlagName: "test", - MapValue: 15, - ContextValueString: "7", - } - c := runTest(t, tis) - expect(t, int64(7), c.Int64("test")) - - c = runRacyTest(t, tis) - refute(t, int64(7), c.Int64("test")) -} - -func TestInt64ApplyInputSourceMethodContextNotSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), - FlagName: "test1", - MapValue: 15, - ContextValueString: "7", - } - c := runTest(t, tis) - expect(t, int64(0), c.Int64("test")) -} - -func TestInt64ApplyInputSourceMethodContextSetInvalidType(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewInt64Flag(&cli.Int64Flag{Name: "test"}), - FlagName: "test", - ContextValueString: "d", - } - c := runTest(t, tis) - expect(t, int64(0), c.Int64("test")) -} - -func TestInt64ApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewInt64Flag(&cli.Int64Flag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: 15, - EnvVarName: "TEST", - EnvVarValue: "12", - } - c := runTest(t, tis) - expect(t, int64(12), c.Int64("test")) - - c = runRacyTest(t, tis) - refute(t, int64(12), c.Int64("test")) -} - -func TestUintApplyInputSourceMethodSet_Alias(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewUintFlag(&cli.UintFlag{Name: "test", Aliases: []string{"test_alias"}}), - FlagName: "test_alias", - MapValue: uint(15), - } - c := runTest(t, tis) - expect(t, uint(15), c.Uint("test_alias")) - - c = runRacyTest(t, tis) - refute(t, uint(15), c.Uint("test_alias")) -} - -func TestUintApplyInputSourceMethodSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewUintFlag(&cli.UintFlag{Name: "test"}), - FlagName: "test", - MapValue: uint(15), - } - c := runTest(t, tis) - expect(t, uint(15), c.Uint("test")) - - c = runRacyTest(t, tis) - refute(t, uint(15), c.Uint("test")) -} - -func TestUintApplyInputSourceMethodContextSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewUintFlag(&cli.UintFlag{Name: "test"}), - FlagName: "test", - MapValue: uint(15), - ContextValueString: "7", - } - c := runTest(t, tis) - expect(t, uint(7), c.Uint("test")) - - c = runRacyTest(t, tis) - refute(t, uint(7), c.Uint("test")) -} - -func TestUint64ApplyInputSourceMethodSet_Alias(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewUint64Flag(&cli.Uint64Flag{Name: "test", Aliases: []string{"test_alias"}}), - FlagName: "test_alias", - MapValue: uint64(15), - } - c := runTest(t, tis) - expect(t, uint64(15), c.Uint64("test_alias")) - - c = runRacyTest(t, tis) - refute(t, uint64(15), c.Uint64("test_alias")) -} - -func TestUint64ApplyInputSourceMethodSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewUint64Flag(&cli.Uint64Flag{Name: "test"}), - FlagName: "test", - MapValue: uint64(15), - } - c := runTest(t, tis) - expect(t, uint64(15), c.Uint64("test")) - - c = runRacyTest(t, tis) - refute(t, uint64(15), c.Uint64("test")) -} - -func TestUint64ApplyInputSourceMethodContextSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewUint64Flag(&cli.Uint64Flag{Name: "test"}), - FlagName: "test", - MapValue: uint64(15), - ContextValueString: "7", - } - c := runTest(t, tis) - expect(t, uint64(7), c.Uint64("test")) - - c = runRacyTest(t, tis) - refute(t, uint64(7), c.Uint64("test")) -} - -func TestUint64ApplyInputSourceMethodEnvVarSet(t *testing.T) { - tis := testApplyInputSource{ - Flag: NewUint64Flag(&cli.Uint64Flag{Name: "test", EnvVars: []string{"TEST"}}), - FlagName: "test", - MapValue: uint64(15), - EnvVarName: "TEST", - EnvVarValue: "12", - } - c := runTest(t, tis) - expect(t, uint64(12), c.Uint64("test")) - - c = runRacyTest(t, tis) - refute(t, uint64(12), c.Uint64("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/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 2ca6055..0000000 --- a/input_source_context.go +++ /dev/null @@ -1,29 +0,0 @@ -package altsrc - -import ( - "time" -) - -// 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) - Int64(name string) (int64, error) - Uint(name string) (uint, error) - Uint64(name string) (uint64, 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) - Float64Slice(name string) ([]float64, error) - Bool(name string) (bool, error) - - isSet(name string) bool -} diff --git a/json_command_test.go b/json_command_test.go deleted file mode 100644 index f0a10e8..0000000 --- a/json_command_test.go +++ /dev/null @@ -1,330 +0,0 @@ -package altsrc - -/* -import ( - "flag" - "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 := os.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 88c57c9..0000000 --- a/json_source_context.go +++ /dev/null @@ -1,307 +0,0 @@ -package altsrc - -import ( - "encoding/json" - "fmt" - "io" - "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 := readURI(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 := io.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) Int64(name string) (int64, 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 int64: - return v, nil - case int: - return int64(v), nil - case float32: - return int64(v), nil - case float64: - return int64(v), nil - } -} - -func (x *jsonSource) Uint(name string) (uint, 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 uint: - return v, nil - case uint64: - return uint(v), nil - case float32: - return uint(v), nil - case float64: - return uint(v), nil - } -} - -func (x *jsonSource) Uint64(name string) (uint64, 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 uint64: - return v, nil - case uint: - return uint64(v), nil - case float32: - return uint64(v), nil - case float64: - return uint64(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) Float64Slice(name string) ([]float64, 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 []float64: - return v, nil - case []interface{}: - c := []float64{} - for _, s := range v { - if i2, ok := s.(float64); 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) 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..e5d84f0 --- /dev/null +++ b/json_value_source_test.go @@ -0,0 +1,50 @@ +package altsrc + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestJSON(t *testing.T) { + r := require.New(t) + + tmpDir := t.TempDir() + + configPath := filepath.Join(tmpDir, "config.json") + altConfigPath := filepath.Join(tmpDir, "alt-config.json") + + r.NoError(os.WriteFile(configPath, []byte(` +{ + "water_fountain": { + "water": false + }, + "woodstock": { + "wood": false + } +} +`), 0644)) + + r.NoError(os.WriteFile(altConfigPath, []byte(` +{ + "water_fountain": { + "water": true + }, + "phone_booth": { + "phone": false + } +} +`), 0644)) + + vsc := YAML( + "water_fountain.water", + "/dev/null/nonexistent.json", + configPath, + altConfigPath, + ) + v, ok := vsc.Lookup() + r.Equal("false", v) + r.True(ok) +} diff --git a/map_input_source.go b/map_input_source.go deleted file mode 100644 index cc3990d..0000000 --- a/map_input_source.go +++ /dev/null @@ -1,450 +0,0 @@ -package altsrc - -import ( - "fmt" - "math" - "strings" - "time" -) - -// 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 -} - -func castToInt64(v interface{}) (int64, bool) { - int64Value := int64(0) - isType := false - if v == nil { - return int64Value, true - } - - // There are only four cases(int, int64, uint64, float64) when parsing the integer in yaml.v3 pkg - // But the cases, uint64, float64, are an error case so that ignored - switch value := v.(type) { - case int: - int64Value = int64(value) - isType = true - case int64: - int64Value = int64(value) - isType = true - } - return int64Value, isType -} - -// Int64 returns an int64 from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Int64(name string) (int64, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := castToInt64(otherGenericValue) - if !isType { - return 0, incorrectTypeForFlagError(name, "int64", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := castToInt64(otherGenericValue) - if !isType { - return 0, incorrectTypeForFlagError(name, "int64", nestedGenericValue) - } - return otherValue, nil - } - - return 0, nil -} - -func castToUint(v interface{}) (uint, bool) { - uintValue := uint(0) - isType := false - if v == nil { - return uintValue, true - } - - // There are only four cases(int, int64, uint64, float64) when parsing the integer in yaml.v3 pkg - // But the last case, float64, is an error case so that ignored - switch value := v.(type) { - case int: - intValue := int(value) - if intValue >= 0 { - uintValue = uint(value) - isType = true - } - case int64: - int64Value := int64(value) - if int64Value >= 0 && uint64(int64Value) <= math.MaxUint { - uintValue = uint(value) - isType = true - } - case uint: - uintValue = uint(value) - isType = true - case uint64: - uint64Value := uint64(value) - if uint64Value <= math.MaxUint { - uintValue = uint(value) - isType = true - } - } - return uintValue, isType -} - -// Int64 returns an int64 from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Uint(name string) (uint, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := castToUint(otherGenericValue) - if !isType { - return 0, incorrectTypeForFlagError(name, "uint", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := castToUint(nestedGenericValue) - if !isType { - return 0, incorrectTypeForFlagError(name, "uint", nestedGenericValue) - } - return otherValue, nil - } - - return 0, nil -} - -func castToUint64(v interface{}) (uint64, bool) { - uint64Value := uint64(0) - isType := false - if v == nil { - return uint64Value, true - } - - // There are only four cases(int, int64, uint64, float64) when parsing the integer in yaml.v3 pkg - // But the last case, float64, is an error case so that ignored - switch value := v.(type) { - case int: - intValue := int(value) - if intValue >= 0 { - uint64Value = uint64(intValue) - isType = true - } - case int64: - int64Value := int64(value) - if int64Value >= 0 { - uint64Value = uint64(int64Value) - isType = true - } - case uint: - uint64Value = uint64(value) - isType = true - case uint64: - uint64Value = uint64(value) - isType = true - } - return uint64Value, isType -} - -// UInt64 returns an uint64 from the map if it exists otherwise returns 0 -func (fsm *MapInputSource) Uint64(name string) (uint64, error) { - otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := castToUint64(otherGenericValue) - if !isType { - return 0, incorrectTypeForFlagError(name, "uint64", otherGenericValue) - } - return otherValue, nil - } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := castToUint64(nestedGenericValue) - if !isType { - return 0, incorrectTypeForFlagError(name, "uint64", 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 := castToInt64(v) - if !isType { - return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int64", v) - } - int64Slice = append(int64Slice, int64Value) - } - - return int64Slice, nil -} - -// Float64Slice returns an []float64 from the map if it exists otherwise returns nil -func (fsm *MapInputSource) Float64Slice(name string) ([]float64, 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 float64Slice = make([]float64, 0, len(otherValue)) - for i, v := range otherValue { - float64Value, isType := v.(float64) - - if !isType { - return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v) - } - - float64Slice = append(float64Slice, float64Value) - } - - return float64Slice, 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 { - return fmt.Errorf("Mismatched type for flag '%s'. Expected '%s' but actual is '%T'", name, expectedTypeName, value) -} diff --git a/map_input_source_test.go b/map_input_source_test.go deleted file mode 100644 index 3ba20c7..0000000 --- a/map_input_source_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package altsrc - -/* -import ( - "fmt" - "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) -} - -func TestMapInputSource_IncorrectFlagTypeError(t *testing.T) { - var testVal *bool - expect(t, incorrectTypeForFlagError("test", "bool", testVal), fmt.Errorf("Mismatched type for flag 'test'. Expected 'bool' but actual is '*bool'")) -} -*/ diff --git a/toml_command_test.go b/toml_command_test.go deleted file mode 100644 index b123ac6..0000000 --- a/toml_command_test.go +++ /dev/null @@ -1,306 +0,0 @@ -package altsrc - -/* -import ( - "flag" - "os" - "testing" - - "github.com/urfave/cli/v3" -) - -func TestCommandTomFileTest(t *testing.T) { - app := &cli.App{} - set := flag.NewFlagSet("test", 0) - _ = os.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) - _ = os.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) - _ = os.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) - _ = os.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) - _ = os.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) - _ = os.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) - _ = os.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) - _ = os.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) - _ = os.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 7d4f3c3..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 := readURI(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..fc769e4 --- /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("unsupported type %#v", v.Kind()) + } + } + return ret, nil +} diff --git a/toml_value_source.go b/toml_value_source.go new file mode 100644 index 0000000..429770c --- /dev/null +++ b/toml_value_source.go @@ -0,0 +1,61 @@ +package altsrc + +import ( + "fmt" + + "github.com/BurntSushi/toml" + "github.com/urfave/cli/v3" +) + +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, nf: newTomlMap}, + }, + ) + } + + 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("yaml 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..0a7372e --- /dev/null +++ b/toml_value_source_test.go @@ -0,0 +1,44 @@ +package altsrc + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTOML(t *testing.T) { + r := require.New(t) + + tmpDir := t.TempDir() + + configPath := filepath.Join(tmpDir, "config.toml") + altConfigPath := filepath.Join(tmpDir, "alt-config.toml") + + r.NoError(os.WriteFile(configPath, []byte(` +[water_fountain] +water = false + +[woodstock] +wood = false +`), 0644)) + + r.NoError(os.WriteFile(altConfigPath, []byte(` +[water_fountain] +water = true + +[phone_booth] +phone = false +`), 0644)) + + vsc := TOML( + "water_fountain.water", + "/dev/null/nonexistent.toml", + configPath, + altConfigPath, + ) + v, ok := vsc.Lookup() + r.Equal("false", v) + r.True(ok) +} diff --git a/yaml_value_source.go b/yaml_value_source.go index 58a1511..4e3fbcc 100644 --- a/yaml_value_source.go +++ b/yaml_value_source.go @@ -2,23 +2,11 @@ package altsrc import ( "fmt" - "io" - "net/http" - "net/url" - "os" - "runtime" - "strings" "github.com/urfave/cli/v3" "gopkg.in/yaml.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...) -} - // YAML is a helper function to encapsulate a number of // yamlValueSource together as a cli.ValueSourceChain func YAML(key string, paths ...string) cli.ValueSourceChain { @@ -30,7 +18,11 @@ func YAML(key string, paths ...string) cli.ValueSourceChain { &yamlValueSource{ file: path, key: key, - ymc: yamlMapInputSourceCache{file: path}, + maafsc: mapAnyAnyFileSourceCache{ + file: path, + nf: newMapAnyAny, + f: yamlUnmarshalFile, + }, }, ) } @@ -38,38 +30,15 @@ func YAML(key string, paths ...string) cli.ValueSourceChain { return vsc } -type yamlMapInputSourceCache struct { - file string - m *map[any]any -} - -func (ymc *yamlMapInputSourceCache) Get() map[any]any { - if ymc.m == nil { - res := map[any]any{} - if err := yamlUnmarshalFile(ymc.file, &res); err == nil { - ymc.m = &res - } else { - tracef("failed to unmarshal yaml from file %[1]q: %[2]v", ymc.file, err) - } - } - - if ymc.m == nil { - tracef("returning empty map") - return map[any]any{} - } - - return *ymc.m -} - type yamlValueSource struct { file string key string - ymc yamlMapInputSourceCache + maafsc mapAnyAnyFileSourceCache } func (yvs *yamlValueSource) Lookup() (string, bool) { - if v, ok := nestedVal(yvs.key, yvs.ymc.Get()); ok { + if v, ok := nestedVal(yvs.key, yvs.maafsc.Get()); ok { return fmt.Sprintf("%[1]v", v), ok } @@ -96,36 +65,3 @@ func yamlUnmarshalFile(filePath string, container any) error { return nil } - -func readURI(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 io.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 os.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 os.ReadFile(filePath) - } - - return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) -} From 608625ddfb6173e6a76d6fc5446cefc25e373801 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 23 Jun 2023 08:02:03 -0400 Subject: [PATCH 05/10] Further cleanups after reviewing --- LICENSE | 2 +- README.md | 12 ++++++++++-- go.sum | 2 -- testdata/empty.yml | 1 - toml_value_source.go | 4 +++- 5 files changed, 14 insertions(+), 7 deletions(-) delete mode 100644 testdata/empty.yml 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/README.md b/README.md index 7d5f62f..610e496 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ -# cli-altsrc +# Welcome to urfave/cli-altsrc -Configuration source integration library for urfave/cli/v3 +[![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.svg)](https://pkg.go.dev/github.com/urfave/cli-altsrc) +[![Go Report Card](https://goreportcard.com/badge/github.com/urfave/cli-altsrc)](https://goreportcard.com/report/github.com/urfave/cli-altsrc) + +urfave/cli-altsrc 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/go.sum b/go.sum index 03f1ef0..462cae7 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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-alpha3 h1:X5l0kjXlvKe3ExllWbcMisze3jDQl98/URz/6uGF3V4= -github.com/urfave/cli/v3 v3.0.0-alpha3/go.mod h1:gHI/xEYplFhOa3Y90xJleh3kqqsSanBj/19hVFxiVZ4= 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= 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_value_source.go b/toml_value_source.go index 429770c..3b76e0c 100644 --- a/toml_value_source.go +++ b/toml_value_source.go @@ -7,6 +7,8 @@ import ( "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{}} @@ -40,7 +42,7 @@ func (tvs *tomlValueSource) Lookup() (string, bool) { } func (tvs *tomlValueSource) String() string { - return fmt.Sprintf("yaml file %[1]q at key %[2]q", tvs.file, tvs.key) + return fmt.Sprintf("toml file %[1]q at key %[2]q", tvs.file, tvs.key) } func (tvs *tomlValueSource) GoString() string { From 1821e18eb1555cc21481398d95f1a688c39b29e1 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 23 Jun 2023 08:34:29 -0400 Subject: [PATCH 06/10] Re-add the `/v3` suffix given that the tags are already out there --- README.md | 6 +++--- examples_test.go | 2 +- go.mod | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 610e496..37b5f24 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Welcome to urfave/cli-altsrc +# Welcome to urfave/cli-altsrc/v3 [![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.svg)](https://pkg.go.dev/github.com/urfave/cli-altsrc) +[![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)](https://goreportcard.com/report/github.com/urfave/cli-altsrc) -urfave/cli-altsrc is an extended value source integration library for [urfave/cli/v3] with support for JSON, +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]. diff --git a/examples_test.go b/examples_test.go index ff389ef..6e9a0b5 100644 --- a/examples_test.go +++ b/examples_test.go @@ -6,7 +6,7 @@ import ( "os" "path/filepath" - altsrc "github.com/urfave/cli-altsrc" + altsrc "github.com/urfave/cli-altsrc/v3" "github.com/urfave/cli/v3" ) diff --git a/go.mod b/go.mod index c3d3a5e..c939294 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/urfave/cli-altsrc +module github.com/urfave/cli-altsrc/v3 go 1.18 From 0bbc55b93b0f1ae3c7e7eb4262dfa445b62449c2 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 23 Jun 2023 09:01:05 -0400 Subject: [PATCH 07/10] Update URL for Go Report Card badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37b5f24..8552700 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![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)](https://goreportcard.com/report/github.com/urfave/cli-altsrc) +[![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 From 73a626280ce8a45e6e9119da37de277c1a21d49f Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 23 Jun 2023 09:13:24 -0400 Subject: [PATCH 08/10] More touch-ups yay --- .github/workflows/main.yml | 4 +++- .gitignore | 10 ++++++++++ Makefile | 14 ++++++++++++++ examples_test.go | 2 +- 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e13e61e..f98f5b7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,4 +17,6 @@ jobs: 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/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/examples_test.go b/examples_test.go index 6e9a0b5..690d61c 100644 --- a/examples_test.go +++ b/examples_test.go @@ -44,7 +44,7 @@ greet: return nil } -func ExampleYAMLValueSource() { +func ExampleYAML_valueSource() { configFiles := []string{ filepath.Join(tmpDir, "config.yaml"), filepath.Join(tmpDir, "alt-config.yaml"), From 1100c1983700023e1a06f5fb9d8fc0b884d5ed40 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 23 Jun 2023 09:24:06 -0400 Subject: [PATCH 09/10] Fill in more examples --- .testdata/alt-config.json | 6 ++ .testdata/alt-config.toml | 3 + .testdata/alt-config.yaml | 3 + .testdata/config.json | 5 ++ .testdata/config.toml | 2 + .testdata/config.yaml | 2 + examples_test.go | 122 +++++++++++++++++++++++++++++++------- 7 files changed, 121 insertions(+), 22 deletions(-) create mode 100644 .testdata/alt-config.json create mode 100644 .testdata/alt-config.toml create mode 100644 .testdata/alt-config.yaml create mode 100644 .testdata/config.json create mode 100644 .testdata/config.toml create mode 100644 .testdata/config.yaml 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/examples_test.go b/examples_test.go index 690d61c..c41df16 100644 --- a/examples_test.go +++ b/examples_test.go @@ -4,50 +4,85 @@ import ( "context" "fmt" "os" + "os/exec" "path/filepath" + "strings" + "time" altsrc "github.com/urfave/cli-altsrc/v3" "github.com/urfave/cli/v3" ) var ( - tmpDir string + testdataDir string ) func init() { - if err := setupYAMLValueSourceExamples(); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + if err := setup(ctx); err != nil { panic(err) } } -func setupYAMLValueSourceExamples() error { - tmpDir = filepath.Join(os.TempDir(), "urfave-cli-altsrc-examples") - if err := os.MkdirAll(tmpDir, 0755); err != nil { +func setup(ctx context.Context) error { + topBytes, err := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel").Output() + if err != nil { return err } - if err := os.WriteFile(filepath.Join(tmpDir, "config.yaml"), []byte(` -greet: - enthusiasm: 9001 -`), 0644); err != nil { - return err + testdataDir = filepath.Join(strings.TrimSpace(string(topBytes)), ".testdata") + return nil +} + +func ExampleYAML() { + configFiles := []string{ + filepath.Join(testdataDir, "config.yaml"), + filepath.Join(testdataDir, "alt-config.yaml"), } - if err := os.WriteFile(filepath.Join(tmpDir, "alt-config.yaml"), []byte(` -greet: - name: Berry - enthusiasm: eleven -`), 0644); err != nil { - return err + 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 + }, } - 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 ExampleYAML_valueSource() { +func ExampleJSON() { configFiles := []string{ - filepath.Join(tmpDir, "config.yaml"), - filepath.Join(tmpDir, "alt-config.yaml"), + filepath.Join(testdataDir, "config.json"), + filepath.Join(testdataDir, "alt-config.json"), } app := &cli.Command{ @@ -56,12 +91,55 @@ func ExampleYAML_valueSource() { &cli.StringFlag{ Name: "name", Aliases: []string{"n"}, - Sources: altsrc.YAML("greet.name", configFiles...), + Sources: altsrc.JSON("greet.name", configFiles...), }, &cli.IntFlag{ Name: "enthusiasm", Aliases: []string{"!"}, - Sources: altsrc.YAML("greet.enthusiasm", configFiles...), + 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 { From 5442352410006f971a7ac09f5dcd894241acf870 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 23 Jun 2023 17:30:33 -0400 Subject: [PATCH 10/10] Changes from feedback, test cleanups, more tests --- .testdata/test_alt_config.json | 8 ++++++++ .testdata/test_alt_config.toml | 5 +++++ .testdata/test_alt_config.yaml | 4 ++++ .testdata/test_config.json | 8 ++++++++ .testdata/test_config.toml | 5 +++++ .testdata/test_config.yaml | 4 ++++ altsrc.go | 37 +++++++++++++++++----------------- altsrc_test.go | 17 ++++++++++++++++ examples_test.go | 29 +++++++------------------- file_source_cache.go | 18 +++++------------ internal/package.go | 26 ++++++++++++++++++++++++ json_value_source_test.go | 33 ++++++------------------------ toml_map.go | 2 +- toml_value_source.go | 7 +++++-- toml_value_source_test.go | 27 ++++++------------------- yaml_value_source.go | 3 +-- yaml_value_source_test.go | 25 ++++++----------------- 17 files changed, 132 insertions(+), 126 deletions(-) create mode 100644 .testdata/test_alt_config.json create mode 100644 .testdata/test_alt_config.toml create mode 100644 .testdata/test_alt_config.yaml create mode 100644 .testdata/test_config.json create mode 100644 .testdata/test_config.toml create mode 100644 .testdata/test_config.yaml create mode 100644 altsrc_test.go create mode 100644 internal/package.go 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/altsrc.go b/altsrc.go index 0d785d1..321902d 100644 --- a/altsrc.go +++ b/altsrc.go @@ -1,6 +1,7 @@ package altsrc import ( + "errors" "fmt" "io" "net/http" @@ -11,6 +12,8 @@ import ( ) var ( + Err = errors.New("urfave/cli-altsrc error") + isTracingOn = os.Getenv("URFAVE_CLI_TRACING") == "on" ) @@ -42,8 +45,8 @@ func tracef(format string, a ...any) { ) } -func readURI(filePath string) ([]byte, error) { - u, err := url.Parse(filePath) +func readURI(uriString string) ([]byte, error) { + u, err := url.Parse(uriString) if err != nil { return nil, err } @@ -51,34 +54,29 @@ func readURI(filePath string) ([]byte, error) { if u.Host != "" { // i have a host, now do i support the scheme? switch u.Scheme { case "http", "https": - res, err := http.Get(filePath) + res, err := http.Get(uriString) if err != nil { return nil, err } return io.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 nil, fmt.Errorf("%[1]w: scheme of %[2]q is unsupported", Err, uriString) } - return os.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) + } 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(filePath) + return os.ReadFile(uriString) } - return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) + 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[interface{}]interface{}) (interface{}, bool) { +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] { @@ -88,12 +86,12 @@ func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool } switch child := child.(type) { - case map[string]interface{}: - node = make(map[interface{}]interface{}, len(child)) + case map[string]any: + node = make(map[any]any, len(child)) for k, v := range child { node[k] = v } - case map[interface{}]interface{}: + case map[any]any: node = child default: return nil, false @@ -103,5 +101,6 @@ func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool 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/examples_test.go b/examples_test.go index c41df16..dc6e0c2 100644 --- a/examples_test.go +++ b/examples_test.go @@ -4,37 +4,22 @@ import ( "context" "fmt" "os" - "os/exec" "path/filepath" - "strings" "time" altsrc "github.com/urfave/cli-altsrc/v3" + "github.com/urfave/cli-altsrc/v3/internal" "github.com/urfave/cli/v3" ) var ( - testdataDir string -) - -func init() { - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - if err := setup(ctx); err != nil { - panic(err) - } -} + testdataDir = func() string { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() -func setup(ctx context.Context) error { - topBytes, err := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel").Output() - if err != nil { - return err - } - - testdataDir = filepath.Join(strings.TrimSpace(string(topBytes)), ".testdata") - return nil -} + return internal.MustTestdataDir(ctx) + }() +) func ExampleYAML() { configFiles := []string{ diff --git a/file_source_cache.go b/file_source_cache.go index cc136e2..eeacb8f 100644 --- a/file_source_cache.go +++ b/file_source_cache.go @@ -3,15 +3,14 @@ package altsrc type fileSourceCache[T any] struct { file string m *T - nf func() T f func(string, any) error } func (fsc *fileSourceCache[T]) Get() T { if fsc.m == nil { - res := fsc.nf() - if err := fsc.f(fsc.file, &res); err == nil { - fsc.m = &res + 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) } @@ -19,20 +18,13 @@ func (fsc *fileSourceCache[T]) Get() T { if fsc.m == nil { tracef("returning empty") - return fsc.nf() + + return *(new(T)) } return *fsc.m } -func newMapAnyAny() map[any]any { - return map[any]any{} -} - type mapAnyAnyFileSourceCache = fileSourceCache[map[any]any] -func newTomlMap() tomlMap { - return tomlMap{Map: map[any]any{}} -} - type tomlMapFileSourceCache = fileSourceCache[tomlMap] 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_value_source_test.go b/json_value_source_test.go index e5d84f0..507f1e4 100644 --- a/json_value_source_test.go +++ b/json_value_source_test.go @@ -1,7 +1,6 @@ package altsrc import ( - "os" "path/filepath" "testing" @@ -11,32 +10,8 @@ import ( func TestJSON(t *testing.T) { r := require.New(t) - tmpDir := t.TempDir() - - configPath := filepath.Join(tmpDir, "config.json") - altConfigPath := filepath.Join(tmpDir, "alt-config.json") - - r.NoError(os.WriteFile(configPath, []byte(` -{ - "water_fountain": { - "water": false - }, - "woodstock": { - "wood": false - } -} -`), 0644)) - - r.NoError(os.WriteFile(altConfigPath, []byte(` -{ - "water_fountain": { - "water": true - }, - "phone_booth": { - "phone": false - } -} -`), 0644)) + configPath := filepath.Join(testdataDir, "test_config.json") + altConfigPath := filepath.Join(testdataDir, "test_alt_config.json") vsc := YAML( "water_fountain.water", @@ -47,4 +22,8 @@ func TestJSON(t *testing.T) { 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/toml_map.go b/toml_map.go index fc769e4..ec14047 100644 --- a/toml_map.go +++ b/toml_map.go @@ -62,7 +62,7 @@ func unmarshalMap(i any) (ret map[any]any, err error) { case reflect.Array, reflect.Slice: ret[key] = val.([]any) default: - return nil, fmt.Errorf("unsupported type %#v", v.Kind()) + 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 index 3b76e0c..2bb1a1a 100644 --- a/toml_value_source.go +++ b/toml_value_source.go @@ -18,7 +18,10 @@ func TOML(key string, paths ...string) cli.ValueSourceChain { &tomlValueSource{ file: path, key: key, - tmc: tomlMapFileSourceCache{file: path, f: tomlUnmarshalFile, nf: newTomlMap}, + tmc: tomlMapFileSourceCache{ + file: path, + f: tomlUnmarshalFile, + }, }, ) } @@ -46,7 +49,7 @@ func (tvs *tomlValueSource) String() string { } func (tvs *tomlValueSource) GoString() string { - return fmt.Sprintf("&tomlValueSource{file:%[1]q,keyPath:%[2]q", tvs.file, tvs.key) + return fmt.Sprintf("&tomlValueSource{file:%[1]q,keyPath:%[2]q}", tvs.file, tvs.key) } func tomlUnmarshalFile(filePath string, container any) error { diff --git a/toml_value_source_test.go b/toml_value_source_test.go index 0a7372e..53023b3 100644 --- a/toml_value_source_test.go +++ b/toml_value_source_test.go @@ -1,7 +1,6 @@ package altsrc import ( - "os" "path/filepath" "testing" @@ -11,26 +10,8 @@ import ( func TestTOML(t *testing.T) { r := require.New(t) - tmpDir := t.TempDir() - - configPath := filepath.Join(tmpDir, "config.toml") - altConfigPath := filepath.Join(tmpDir, "alt-config.toml") - - r.NoError(os.WriteFile(configPath, []byte(` -[water_fountain] -water = false - -[woodstock] -wood = false -`), 0644)) - - r.NoError(os.WriteFile(altConfigPath, []byte(` -[water_fountain] -water = true - -[phone_booth] -phone = false -`), 0644)) + configPath := filepath.Join(testdataDir, "test_config.toml") + altConfigPath := filepath.Join(testdataDir, "test_alt_config.toml") vsc := TOML( "water_fountain.water", @@ -41,4 +22,8 @@ phone = false 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_value_source.go b/yaml_value_source.go index 4e3fbcc..3736915 100644 --- a/yaml_value_source.go +++ b/yaml_value_source.go @@ -20,7 +20,6 @@ func YAML(key string, paths ...string) cli.ValueSourceChain { key: key, maafsc: mapAnyAnyFileSourceCache{ file: path, - nf: newMapAnyAny, f: yamlUnmarshalFile, }, }, @@ -50,7 +49,7 @@ func (yvs *yamlValueSource) String() string { } func (yvs *yamlValueSource) GoString() string { - return fmt.Sprintf("&yamlValueSource{file:%[1]q,keyPath:%[2]q", yvs.file, yvs.key) + return fmt.Sprintf("&yamlValueSource{file:%[1]q,keyPath:%[2]q}", yvs.file, yvs.key) } func yamlUnmarshalFile(filePath string, container any) error { diff --git a/yaml_value_source_test.go b/yaml_value_source_test.go index 311e808..cdc94c0 100644 --- a/yaml_value_source_test.go +++ b/yaml_value_source_test.go @@ -1,7 +1,6 @@ package altsrc import ( - "os" "path/filepath" "testing" @@ -11,24 +10,8 @@ import ( func TestYAML(t *testing.T) { r := require.New(t) - tmpDir := t.TempDir() - - configPath := filepath.Join(tmpDir, "config.yaml") - altConfigPath := filepath.Join(tmpDir, "alt-config.yaml") - - r.NoError(os.WriteFile(configPath, []byte(` -water_fountain: - water: false -woodstock: - wood: false -`), 0644)) - - r.NoError(os.WriteFile(altConfigPath, []byte(` -water_fountain: - water: true -phone_booth: - phone: false -`), 0644)) + configPath := filepath.Join(testdataDir, "test_config.yaml") + altConfigPath := filepath.Join(testdataDir, "test_alt_config.yaml") vsc := YAML( "water_fountain.water", @@ -39,4 +22,8 @@ phone_booth: 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()) }