Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Autodetect config format based on extension #1702

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
67 changes: 67 additions & 0 deletions altsrc/detect_input_source.go
@@ -0,0 +1,67 @@
package altsrc

import (
"fmt"
"path/filepath"
"sort"

"github.com/urfave/cli/v2"
)

// defaultSources is a read-only map, making it concurrency-safe
var defaultSources = map[string]func(string) func(*cli.Context) (InputSourceContext, error){
".conf": NewTomlSourceFromFlagFunc,
".json": NewJSONSourceFromFlagFunc,
".toml": NewTomlSourceFromFlagFunc,
".yaml": NewYamlSourceFromFlagFunc,
".yml": NewYamlSourceFromFlagFunc,
}

// DetectNewSourceFromFlagFunc creates a new InputSourceContext from a provided flag name and source context.
func DetectNewSourceFromFlagFunc(flagFileName string) func(*cli.Context) (InputSourceContext, error) {
return func(cCtx *cli.Context) (InputSourceContext, error) {
if fileFullPath := cCtx.String(flagFileName); fileFullPath != "" {
detectableSources := make(map[string]func(string) func(*cli.Context) (InputSourceContext, error))
fileExt := filepath.Ext(fileFullPath)

// Check if the App contains a handler for this extension first, allowing it to override the defaults
detectExt, isType := cCtx.App.GetExtension("DetectableSources").(DetectableSourcesAppExtension)
if isType {
detectableSources = detectExt.getDetectableSources()

if handler, ok := detectableSources[fileExt]; ok {
return handler(flagFileName)(cCtx)
}
}

// Fall back to the default sources implemented by the library itself
if handler, ok := defaultSources[fileExt]; ok {
return handler(flagFileName)(cCtx)
}

return nil, fmt.Errorf("Unable to determine config file type from extension.\nMust be one of %s", detectableExtensions(detectableSources))
}

return defaultInputSource()
}
}

func detectableExtensions(detectableSources map[string]func(string) func(*cli.Context) (InputSourceContext, error)) []string {
// We don't preallocate because this generates empty space in the output
// It's less efficient, but this is for error messaging only at the moment
var extensions []string

for ext := range detectableSources {
extensions = append(extensions, ext)
}
for ext := range defaultSources {
// Only add sources that haven't been overridden by the App
if _, ok := detectableSources[ext]; !ok {
extensions = append(extensions, ext)
}
}

sort.Strings(extensions)

return extensions
}
316 changes: 316 additions & 0 deletions altsrc/detect_input_source_test.go
@@ -0,0 +1,316 @@
package altsrc

import (
"flag"
"fmt"
"os"
"testing"

"github.com/urfave/cli/v2"
)

func TestDetectsConfCorrectly(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
_ = os.WriteFile("current.conf", []byte("test = 15"), 0666)
defer os.Remove("current.conf")
test := []string{"test-cmd", "--load", "current.conf"}
_ = 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, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, nil)
}

func TestDetectsJsonCorrectly(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
_ = os.WriteFile("current.json", []byte("{\"test\":15}"), 0666)
defer os.Remove("current.json")
test := []string{"test-cmd", "--load", "current.json"}
_ = 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, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, nil)
}

func TestDetectsTomlCorrectly(t *testing.T) {
app := cli.NewApp()
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, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, nil)
}

func TestDetectsYamlCorrectly(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
_ = os.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, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, nil)
}

func TestDetectsYmlCorrectly(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
_ = os.WriteFile("current.yml", []byte("test: 15"), 0666)
defer os.Remove("current.yml")
test := []string{"test-cmd", "--load", "current.yml"}
_ = 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, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, nil)
}

func TestHandlesCustomTypeCorrectly(t *testing.T) {
app := cli.NewApp()
app.AddExtension(NewDetectableSourcesAppExtension())
app.GetExtension("DetectableSources").(DetectableSourcesAppExtension).RegisterDetectableSource(".custom", NewYamlSourceFromFlagFunc)
set := flag.NewFlagSet("test", 0)
_ = os.WriteFile("current.custom", []byte("test: 15"), 0666)
defer os.Remove("current.custom")
test := []string{"test-cmd", "--load", "current.custom"}
_ = 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, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, nil)
}

func TestAllowsOverrides(t *testing.T) {
app := cli.NewApp()
app.AddExtension(NewDetectableSourcesAppExtension())
app.GetExtension("DetectableSources").(DetectableSourcesAppExtension).RegisterDetectableSource(".conf", NewYamlSourceFromFlagFunc)
set := flag.NewFlagSet("test", 0)
_ = os.WriteFile("current.conf", []byte("test: 15"), 0666)
defer os.Remove("current.conf")
test := []string{"test-cmd", "--load", "current.conf"}
_ = 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, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, nil)
}

func TestFailsOnUnrocegnized(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
_ = os.WriteFile("current.fake", []byte("test: 15"), 0666)
defer os.Remove("current.fake")
test := []string{"test-cmd", "--load", "current.fake"}
_ = 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, 0)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test"}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, fmt.Errorf("Unable to create input source with context: inner error: \n'Unable to determine config file type from extension.\nMust be one of [.conf .json .toml .yaml .yml]'"))
}

func TestSilentNoOpWithoutFlag(t *testing.T) {
app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
_ = os.WriteFile("current.conf", []byte("test = 15"), 0666)
defer os.Remove("current.conf")
test := []string{"test-cmd"}
_ = 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, 0)
return nil
},
Flags: []cli.Flag{
NewIntFlag(&cli.IntFlag{Name: "test"}),
&cli.StringFlag{Name: "load"}},
}
command.Before = InitInputSourceWithContext(command.Flags, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, nil)
}

func TestLoadDefaultConfig(t *testing.T) {
t.Skip("Fix parent implementation for default Flag values to get this working")

app := cli.NewApp()
set := flag.NewFlagSet("test", 0)
_ = os.WriteFile("current.conf", []byte("test = 15"), 0666)
defer os.Remove("current.conf")
test := []string{"test-cmd"}
_ = 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", Value: "current.conf"}},
}
command.Before = InitInputSourceWithContext(command.Flags, DetectNewSourceFromFlagFunc("load"))
err := command.Run(c, test...)

expect(t, err, nil)
}