Repository: spf13/viper
Pull request #1852 (2024-06-04 13:30:08)

Finder feature flag
This PR improves how the file searching behavior is affected by the new Finder component.

Everything should work the same by default.

The new Finder component should be used when it's configured using `WithFinder`.

When using `ExperimentalFinder`, the default search API should use the finder under the hood.

github-actions[bot]: 👋 Thanks for contributing to Viper! You are awesome! 🎉

A maintainer will take a look at your pull request shortly. 👀

In the meantime: We are working on **Viper v2** and we would love to hear your thoughts about what you like or don't like about Viper, so we can improve or fix those issues.

⏰ If you have a couple minutes, please take some time and share your thoughts: https://forms.gle/R6faU74qPRPAzchZ9

📣 If you've already given us your feedback, you can still help by spreading the news,
either by sharing the above link or telling people about this on Twitter:

https://twitter.com/sagikazarmark/status/1306904078967074816

**Thank you!** ❤️


Patch:
diff --git a/file.go b/file.go
index 101624d..50a4058 100644
--- a/file.go
+++ b/file.go
@@ -1,5 +1,3 @@
-//go:build !viper_finder
-
 package viper
 
 import (
@@ -7,12 +5,62 @@ import (
 	"os"
 	"path/filepath"
 
+	"github.com/sagikazarmark/locafero"
 	"github.com/spf13/afero"
 )
 
+// ExperimentalFinder tells Viper to use the new Finder interface for finding configuration files.
+func ExperimentalFinder() Option {
+	return optionFunc(func(v *Viper) {
+		v.experimentalFinder = true
+	})
+}
+
+// Search for a config file.
+func (v *Viper) findConfigFile() (string, error) {
+	finder := v.finder
+
+	if finder == nil && v.experimentalFinder {
+		var names []string
+
+		if v.configType != "" {
+			names = locafero.NameWithOptionalExtensions(v.configName, SupportedExts...)
+		} else {
+			names = locafero.NameWithExtensions(v.configName, SupportedExts...)
+		}
+
+		finder = locafero.Finder{
+			Paths: v.configPaths,
+			Names: names,
+			Type:  locafero.FileTypeFile,
+		}
+	}
+
+	if finder != nil {
+		return v.findConfigFileWithFinder(finder)
+	}
+
+	return v.findConfigFileOld()
+}
+
+func (v *Viper) findConfigFileWithFinder(finder Finder) (string, error) {
+	results, err := finder.Find(v.fs)
+	if err != nil {
+		return "", err
+	}
+
+	if len(results) == 0 {
+		return "", ConfigFileNotFoundError{v.configName, fmt.Sprintf("%s", v.configPaths)}
+	}
+
+	// We call clean on the final result to ensure that the path is in its canonical form.
+	// This is mostly for consistent path handling and to make sure tests pass.
+	return results[0], nil
+}
+
 // Search all configPaths for any config file.
 // Returns the first path that exists (and is a config file).
-func (v *Viper) findConfigFile() (string, error) {
+func (v *Viper) findConfigFileOld() (string, error) {
 	v.logger.Info("searching for config in paths", "paths", v.configPaths)
 
 	for _, cp := range v.configPaths {
diff --git a/file_finder.go b/file_finder.go
deleted file mode 100644
index c873621..0000000
--- a/file_finder.go
+++ /dev/null
@@ -1,42 +0,0 @@
-//go:build viper_finder
-
-package viper
-
-import (
-	"fmt"
-
-	"github.com/sagikazarmark/locafero"
-)
-
-// Search all configPaths for any config file.
-// Returns the first path that exists (and is a config file).
-func (v *Viper) findConfigFile() (string, error) {
-	finder := v.finder
-
-	if finder == nil {
-		var names []string
-
-		if v.configType != "" {
-			names = locafero.NameWithOptionalExtensions(v.configName, SupportedExts...)
-		} else {
-			names = locafero.NameWithExtensions(v.configName, SupportedExts...)
-		}
-
-		finder = locafero.Finder{
-			Paths: v.configPaths,
-			Names: names,
-			Type:  locafero.FileTypeFile,
-		}
-	}
-
-	results, err := finder.Find(v.fs)
-	if err != nil {
-		return "", err
-	}
-
-	if len(results) == 0 {
-		return "", ConfigFileNotFoundError{v.configName, fmt.Sprintf("%s", v.configPaths)}
-	}
-
-	return results[0], nil
-}
diff --git a/go.mod b/go.mod
index 2cc6b08..76fe4c6 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	github.com/magiconair/properties v1.8.7
 	github.com/pelletier/go-toml/v2 v2.2.2
 	github.com/sagikazarmark/crypt v0.20.0
-	github.com/sagikazarmark/locafero v0.5.0
+	github.com/sagikazarmark/locafero v0.6.0
 	github.com/spf13/afero v1.11.0
 	github.com/spf13/cast v1.6.0
 	github.com/spf13/pflag v1.0.5
diff --git a/go.sum b/go.sum
index 5b1e700..1d84624 100644
--- a/go.sum
+++ b/go.sum
@@ -246,8 +246,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sagikazarmark/crypt v0.20.0 h1:aMLNL5RDUyk+/Bmoql3iYbv6C75/H0qvs6CWHzRGo6Y=
 github.com/sagikazarmark/crypt v0.20.0/go.mod h1:ojgvzJOpJkRhzSrycMyk8cgctQgyfZN/d2/dVh4U68c=
-github.com/sagikazarmark/locafero v0.5.0 h1:zXz2JnQDgE5gDg0R9ThkNT0orQzm47i8IuO6hk6XSYY=
-github.com/sagikazarmark/locafero v0.5.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
+github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
+github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
diff --git a/internal/features/finder.go b/internal/features/finder.go
new file mode 100644
index 0000000..983ea3a
--- /dev/null
+++ b/internal/features/finder.go
@@ -0,0 +1,5 @@
+//go:build viper_finder
+
+package features
+
+const Finder = true
diff --git a/internal/features/finder_default.go b/internal/features/finder_default.go
new file mode 100644
index 0000000..89bcb06
--- /dev/null
+++ b/internal/features/finder_default.go
@@ -0,0 +1,5 @@
+//go:build !viper_finder
+
+package features
+
+const Finder = false
diff --git a/viper.go b/viper.go
index 472fd59..3f9d164 100644
--- a/viper.go
+++ b/viper.go
@@ -194,6 +194,8 @@ type Viper struct {
 	// TODO: should probably be protected with a mutex
 	encoderRegistry *encoding.EncoderRegistry
 	decoderRegistry *encoding.DecoderRegistry
+
+	experimentalFinder bool
 }
 
 // New returns an initialized Viper instance.
@@ -216,6 +218,8 @@ func New() *Viper {
 
 	v.resetEncoding()
 
+	v.experimentalFinder = features.Finder
+
 	return v
 }
 


Test patch:
diff --git a/finder_example_test.go b/finder_example_test.go
index 76c5f57..8f09452 100644
--- a/finder_example_test.go
+++ b/finder_example_test.go
@@ -1,5 +1,3 @@
-//go:build viper_finder
-
 package viper_test
 
 import (
diff --git a/viper_test.go b/viper_test.go
index 33e6cc7..184f0b3 100644
--- a/viper_test.go
+++ b/viper_test.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/fsnotify/fsnotify"
 	"github.com/go-viper/mapstructure/v2"
+	"github.com/sagikazarmark/locafero"
 	"github.com/spf13/afero"
 	"github.com/spf13/cast"
 	"github.com/spf13/pflag"
@@ -363,6 +364,54 @@ func TestGetConfigFile(t *testing.T) {
 		// are not considered
 		assert.Error(t, err)
 	})
+
+	t.Run("experimental finder", func(t *testing.T) {
+		fs := afero.NewMemMapFs()
+
+		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
+		require.NoError(t, err)
+
+		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
+		require.NoError(t, err)
+
+		v := NewWithOptions(ExperimentalFinder())
+
+		v.SetFs(fs)
+
+		v.AddConfigPath("/etc/viper")
+
+		filename, err := v.getConfigFile()
+		assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), testutil.AbsFilePath(t, filename))
+		assert.NoError(t, err)
+	})
+
+	t.Run("finder", func(t *testing.T) {
+		fs := afero.NewMemMapFs()
+
+		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
+		require.NoError(t, err)
+
+		_, err = fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
+		require.NoError(t, err)
+
+		finder := locafero.Finder{
+			Paths: []string{testutil.AbsFilePath(t, "/etc/viper")},
+			Names: locafero.NameWithExtensions("config", SupportedExts...),
+			Type:  locafero.FileTypeFile,
+		}
+
+		v := NewWithOptions(WithFinder(finder))
+
+		v.SetFs(fs)
+
+		// These should be ineffective
+		v.AddConfigPath("/etc/something_else")
+		v.SetConfigName("not-config")
+
+		filename, err := v.getConfigFile()
+		assert.Equal(t, testutil.AbsFilePath(t, "/etc/viper/config.yaml"), testutil.AbsFilePath(t, filename))
+		assert.NoError(t, err)
+	})
 }
 
 func TestReadInConfig(t *testing.T) {
@@ -415,6 +464,65 @@ func TestReadInConfig(t *testing.T) {
 
 		assert.Equal(t, "value", v.Get("key"))
 	})
+
+	t.Run("find file with experimental finder", func(t *testing.T) {
+		fs := afero.NewMemMapFs()
+
+		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
+		require.NoError(t, err)
+
+		file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
+		require.NoError(t, err)
+
+		_, err = file.WriteString(`key: value`)
+		require.NoError(t, err)
+
+		file.Close()
+
+		v := NewWithOptions(ExperimentalFinder())
+
+		v.SetFs(fs)
+		v.AddConfigPath("/etc/viper")
+
+		err = v.ReadInConfig()
+		require.NoError(t, err)
+
+		assert.Equal(t, "value", v.Get("key"))
+	})
+
+	t.Run("find file using a finder", func(t *testing.T) {
+		fs := afero.NewMemMapFs()
+
+		err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
+		require.NoError(t, err)
+
+		file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
+		require.NoError(t, err)
+
+		_, err = file.WriteString(`key: value`)
+		require.NoError(t, err)
+
+		file.Close()
+
+		finder := locafero.Finder{
+			Paths: []string{testutil.AbsFilePath(t, "/etc/viper")},
+			Names: locafero.NameWithExtensions("config", SupportedExts...),
+			Type:  locafero.FileTypeFile,
+		}
+
+		v := NewWithOptions(WithFinder(finder))
+
+		v.SetFs(fs)
+
+		// These should be ineffective
+		v.AddConfigPath("/etc/something_else")
+		v.SetConfigName("not-config")
+
+		err = v.ReadInConfig()
+		require.NoError(t, err)
+
+		assert.Equal(t, "value", v.Get("key"))
+	})
 }
 
 func TestDefault(t *testing.T) {