Repository: spf13/viper
Pull request #1856 (2024-06-05 07:33:18)

Allow setting options on the global Viper instance
Fixes #1581

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/README.md b/README.md
index 031ce1d..c2b2237 100644
--- a/README.md
+++ b/README.md
@@ -836,13 +836,15 @@ func yamlStringSettings() string {
 
 ## Viper or Vipers?
 
-Viper comes ready to use out of the box. There is no configuration or
-initialization needed to begin using Viper. Since most applications will want
-to use a single central repository for their configuration, the viper package
-provides this. It is similar to a singleton.
+Viper comes with a global instance (singleton) out of the box.
 
-In all of the examples above, they demonstrate using viper in its singleton
-style approach.
+Although it makes setting up configuration easy,
+using it is generally discouraged as it makes testing harder and can lead to unexpected behavior.
+
+The best practice is to initialize a Viper instance and pass that around when necessary.
+
+The global instance _MAY_ be deprecated in the future.
+See [#1855](https://github.com/spf13/viper/issues/1855) for more details.
 
 ### Working with multiple vipers
 
diff --git a/viper.go b/viper.go
index 3f9d164..f5a3163 100644
--- a/viper.go
+++ b/viper.go
@@ -271,6 +271,23 @@ func NewWithOptions(opts ...Option) *Viper {
 	return v
 }
 
+// SetOptions sets the options on the global Viper instance.
+//
+// Be careful when using this function: subsequent calls may override options you set.
+// It's always better to use a local Viper instance.
+func SetOptions(opts ...Option) {
+	keyDelim := v.keyDelim
+
+	for _, opt := range opts {
+		opt.apply(v)
+	}
+
+	// reset encoding if key delimiter changed
+	if keyDelim != v.keyDelim {
+		v.resetEncoding()
+	}
+}
+
 // Reset is intended for testing, will reset all to default settings.
 // In the public interface for the viper package so applications
 // can use it in their testing as well.
@@ -1577,6 +1594,10 @@ func (v *Viper) MergeInConfig() error {
 func ReadConfig(in io.Reader) error { return v.ReadConfig(in) }
 
 func (v *Viper) ReadConfig(in io.Reader) error {
+	if v.configType == "" {
+		return errors.New("cannot decode configuration: config type is not set")
+	}
+
 	v.config = make(map[string]any)
 	return v.unmarshalReader(in, v.config)
 }
@@ -1585,6 +1606,10 @@ func (v *Viper) ReadConfig(in io.Reader) error {
 func MergeConfig(in io.Reader) error { return v.MergeConfig(in) }
 
 func (v *Viper) MergeConfig(in io.Reader) error {
+	if v.configType == "" {
+		return errors.New("cannot decode configuration: config type is not set")
+	}
+
 	cfg := make(map[string]any)
 	if err := v.unmarshalReader(in, cfg); err != nil {
 		return err


Test patch:
diff --git a/viper_test.go b/viper_test.go
index 184f0b3..0b9d9c6 100644
--- a/viper_test.go
+++ b/viper_test.go
@@ -1641,20 +1641,29 @@ func TestFindsNestedKeys(t *testing.T) {
 	}
 }
 
-func TestReadBufConfig(t *testing.T) {
-	v := New()
-	v.SetConfigType("yaml")
-	v.ReadConfig(bytes.NewBuffer(yamlExample))
-	t.Log(v.AllKeys())
+func TestReadConfig(t *testing.T) {
+	t.Run("ok", func(t *testing.T) {
+		v := New()
+		v.SetConfigType("yaml")
+		err := v.ReadConfig(bytes.NewBuffer(yamlExample))
+		require.NoError(t, err)
+		t.Log(v.AllKeys())
+
+		assert.True(t, v.InConfig("name"))
+		assert.True(t, v.InConfig("clothing.jacket"))
+		assert.False(t, v.InConfig("state"))
+		assert.False(t, v.InConfig("clothing.hat"))
+		assert.Equal(t, "steve", v.Get("name"))
+		assert.Equal(t, []any{"skateboarding", "snowboarding", "go"}, v.Get("hobbies"))
+		assert.Equal(t, map[string]any{"jacket": "leather", "trousers": "denim", "pants": map[string]any{"size": "large"}}, v.Get("clothing"))
+		assert.Equal(t, 35, v.Get("age"))
+	})
 
-	assert.True(t, v.InConfig("name"))
-	assert.True(t, v.InConfig("clothing.jacket"))
-	assert.False(t, v.InConfig("state"))
-	assert.False(t, v.InConfig("clothing.hat"))
-	assert.Equal(t, "steve", v.Get("name"))
-	assert.Equal(t, []any{"skateboarding", "snowboarding", "go"}, v.Get("hobbies"))
-	assert.Equal(t, map[string]any{"jacket": "leather", "trousers": "denim", "pants": map[string]any{"size": "large"}}, v.Get("clothing"))
-	assert.Equal(t, 35, v.Get("age"))
+	t.Run("missing config type", func(t *testing.T) {
+		v := New()
+		err := v.ReadConfig(bytes.NewBuffer(yamlExample))
+		require.Error(t, err)
+	})
 }
 
 func TestIsSet(t *testing.T) {
@@ -2127,6 +2136,7 @@ func TestSafeWriteAsConfig(t *testing.T) {
 	v := New()
 	fs := afero.NewMemMapFs()
 	v.SetFs(fs)
+	v.SetConfigType("yaml")
 	err := v.ReadConfig(bytes.NewBuffer(yamlExample))
 	require.NoError(t, err)
 	require.NoError(t, v.SafeWriteConfigAs("/test/c.yaml"))