Repository: spf13/viper
Pull request #794 (2019-11-06 11:45:17)

Add key delimiter setter
This PR adds a new constructor for Viper and a functional option for the key delimiter. This is an initial solution for supporting keys containing the key delimiter.

For the mid-term, I would probably add back the feature reverted in #771 (and fixed in #766) with a feature flag `v.AllowDelimiterInKey` defaulting to false. That changes the behavior of Viper though (see https://github.com/spf13/viper/pull/766#issuecomment-532644550) which is why we need the flag, defaulting to false.

cc @inkychris @mschneider82


Patch:
diff --git a/README.md b/README.md
index 3be3b4b..b0392c9 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,10 @@
 
 Go configuration with fangs!
 
+[![Actions](https://github.com/spf13/viper/workflows/CI/badge.svg)](https://github.com/spf13/viper)
+[![Join the chat at https://gitter.im/spf13/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![GoDoc](https://godoc.org/github.com/spf13/viper?status.svg)](https://godoc.org/github.com/spf13/viper)
+
 Many Go projects are built using Viper including:
 
 * [Hugo](http://gohugo.io)
@@ -14,13 +18,14 @@ Many Go projects are built using Viper including:
 * [Clairctl](https://github.com/jgsqware/clairctl)
 * [Mercure](https://mercure.rocks)
 
-[![Build Status](https://travis-ci.org/spf13/viper.svg)](https://travis-ci.org/spf13/viper) [![Join the chat at https://gitter.im/spf13/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GoDoc](https://godoc.org/github.com/spf13/viper?status.svg)](https://godoc.org/github.com/spf13/viper)
 
 ## Install
+
 ```console
-go get -u github.com/spf13/viper
+go get github.com/spf13/viper
 ```
 
+
 ## What is Viper?
 
 Viper is a complete configuration solution for Go applications including 12-Factor apps. It is designed
@@ -36,8 +41,8 @@ and formats. It supports:
 * reading from buffer
 * setting explicit values
 
-Viper can be thought of as a registry for all of your applications
-configuration needs.
+Viper can be thought of as a registry for all of your applications configuration needs.
+
 
 ## Why Viper?
 
@@ -47,34 +52,31 @@ Viper is here to help with that.
 
 Viper does the following for you:
 
-1. Find, load, and unmarshal a configuration file in JSON, TOML, YAML, HCL, envfile or Java properties formats.
-2. Provide a mechanism to set default values for your different
-   configuration options.
-3. Provide a mechanism to set override values for options specified through
-   command line flags.
-4. Provide an alias system to easily rename parameters without breaking existing
-   code.
-5. Make it easy to tell the difference between when a user has provided a
-   command line or config file which is the same as the default.
+1. Find, load, and unmarshal a configuration file in JSON, TOML, YAML, HCL, INI, envfile or Java properties formats.
+2. Provide a mechanism to set default values for your different configuration options.
+3. Provide a mechanism to set override values for options specified through command line flags.
+4. Provide an alias system to easily rename parameters without breaking existing code.
+5. Make it easy to tell the difference between when a user has provided a command line or config file which is the same as the default.
 
-Viper uses the following precedence order. Each item takes precedence over the
-item below it:
+Viper uses the following precedence order. Each item takes precedence over the item below it:
 
- * explicit call to Set
+ * explicit call to `Set`
  * flag
  * env
  * config
  * key/value store
  * default
 
-Viper configuration keys are case insensitive.
+**Important:** Viper configuration keys are case insensitive.
+There are ongoing discussions about making that optional.
+
 
 ## Putting Values into Viper
 
 ### Establishing Defaults
 
 A good configuration system will support default values. A default value is not
-required for a key, but it’s useful in the event that a key hasn’t been set via
+required for a key, but it’s useful in the event that a key hasn't been set via
 config file, environment variable, remote configuration or flag.
 
 Examples:
@@ -88,7 +90,7 @@ viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "cat
 ### Reading Config Files
 
 Viper requires minimal configuration so it knows where to look for config files.
-Viper supports JSON, TOML, YAML, HCL, envfile and Java Properties files. Viper can search multiple paths, but
+Viper supports JSON, TOML, YAML, HCL, INI, envfile and Java Properties files. Viper can search multiple paths, but
 currently a single Viper instance only supports a single configuration file.
 Viper does not default to any configuration search paths leaving defaults decision
 to an application.
@@ -661,6 +663,32 @@ if err != nil {
 }
 ```
 
+If you want to unmarshal configuration where the keys themselves contain dot (the default key delimiter),
+you have to change the delimiter:
+
+```go
+v := viper.NewWithOptions(viper.KeyDelimiter("::"))
+
+v.SetDefault("chart::values", map[string]interface{}{
+    "ingress": map[string]interface{}{
+        "annotations": map[string]interface{}{
+            "traefik.frontend.rule.type":                 "PathPrefix",
+            "traefik.ingress.kubernetes.io/ssl-redirect": "true",
+        },
+    },
+})
+
+type config struct {
+	Chart struct{
+        Values map[string]interface{}
+    }
+}
+
+var C config
+
+v.Unmarshal(&C)
+```
+
 Viper uses [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) under the hood for unmarshaling values which uses `mapstructure` tags by default.
 
 ### Marshalling to string
@@ -718,13 +746,6 @@ different vipers.
 
 ## Q & A
 
-Q: Why not INI files?
-
-A: Ini files are pretty awful. There’s no standard format, and they are hard to
-validate. Viper is designed to work with JSON, TOML or YAML files. If someone
-really wants to add this feature, I’d be happy to merge it. It’s easy to specify
-which formats your application will permit.
-
 Q: Why is it called “Viper”?
 
 A: Viper is designed to be a [companion](http://en.wikipedia.org/wiki/Viper_(G.I._Joe))
diff --git a/viper.go b/viper.go
index a944cde..4e4d6da 100644
--- a/viper.go
+++ b/viper.go
@@ -34,20 +34,19 @@ import (
 	"sync"
 	"time"
 
-	yaml "gopkg.in/yaml.v2"
-
 	"github.com/fsnotify/fsnotify"
 	"github.com/hashicorp/hcl"
 	"github.com/hashicorp/hcl/hcl/printer"
 	"github.com/magiconair/properties"
 	"github.com/mitchellh/mapstructure"
-	toml "github.com/pelletier/go-toml"
+	"github.com/pelletier/go-toml"
 	"github.com/spf13/afero"
 	"github.com/spf13/cast"
 	jww "github.com/spf13/jwalterweatherman"
 	"github.com/spf13/pflag"
 	"github.com/subosito/gotenv"
 	"gopkg.in/ini.v1"
+	"gopkg.in/yaml.v2"
 )
 
 // ConfigMarshalError happens when failing to marshal the configuration.
@@ -236,6 +235,39 @@ func New() *Viper {
 	return v
 }
 
+// Option configures Viper using the functional options paradigm popularized by Rob Pike and Dave Cheney.
+// If you're unfamiliar with this style,
+// see https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html and
+// https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis.
+type Option interface {
+	apply(v *Viper)
+}
+
+type optionFunc func(v *Viper)
+
+func (fn optionFunc) apply(v *Viper) {
+	fn(v)
+}
+
+// KeyDelimiter sets the delimiter used for determining key parts.
+// By default it's value is ".".
+func KeyDelimiter(d string) Option {
+	return optionFunc(func(v *Viper) {
+		v.keyDelim = d
+	})
+}
+
+// NewWithOptions creates a new Viper instance.
+func NewWithOptions(opts ...Option) *Viper {
+	v := New()
+
+	for _, opt := range opts {
+		opt.apply(v)
+	}
+
+	return v
+}
+
 // 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.
@@ -1720,7 +1752,7 @@ func (v *Viper) getRemoteConfig(provider RemoteProvider) (map[string]interface{}
 func (v *Viper) watchKeyValueConfigOnChannel() error {
 	for _, rp := range v.remoteProviders {
 		respc, _ := RemoteConfig.WatchChannel(rp)
-		//Todo: Add quit channel
+		// Todo: Add quit channel
 		go func(rc <-chan *RemoteResponse) {
 			for {
 				b := <-rc
@@ -1756,7 +1788,7 @@ func (v *Viper) watchRemoteConfig(provider RemoteProvider) (map[string]interface
 }
 
 // AllKeys returns all keys holding a value, regardless of where they are set.
-// Nested keys are returned with a v.keyDelim (= ".") separator
+// Nested keys are returned with a v.keyDelim separator
 func AllKeys() []string { return v.AllKeys() }
 func (v *Viper) AllKeys() []string {
 	m := map[string]bool{}
@@ -1779,7 +1811,7 @@ func (v *Viper) AllKeys() []string {
 
 // flattenAndMergeMap recursively flattens the given map into a map[string]bool
 // of key paths (used as a set, easier to manipulate than a []string):
-// - each path is merged into a single key string, delimited with v.keyDelim (= ".")
+// - each path is merged into a single key string, delimited with v.keyDelim
 // - if a path is shadowed by an earlier value in the initial shadow map,
 //   it is skipped.
 // The resulting set of paths is merged to the given shadow set at the same time.


Test patch:
diff --git a/viper_test.go b/viper_test.go
index c1cf557..9f4dff3 100644
--- a/viper_test.go
+++ b/viper_test.go
@@ -2009,6 +2009,73 @@ func TestUnmarshal_DotSeparatorBackwardCompatibility(t *testing.T) {
 	assert.Equal(t, "cobra_flag", config.Foo.Bar)
 }
 
+var yamlExampleWithDot = []byte(`Hacker: true
+name: steve
+hobbies:
+  - skateboarding
+  - snowboarding
+  - go
+clothing:
+  jacket: leather
+  trousers: denim
+  pants:
+    size: large
+age: 35
+eyes : brown
+beard: true
+emails:
+  steve@hacker.com:
+    created: 01/02/03
+    active: true
+`)
+
+func TestKeyDelimiter(t *testing.T) {
+	v := NewWithOptions(KeyDelimiter("::"))
+	v.SetConfigType("yaml")
+	r := strings.NewReader(string(yamlExampleWithDot))
+
+	err := v.unmarshalReader(r, v.config)
+	require.NoError(t, err)
+
+	values := map[string]interface{}{
+		"image": map[string]interface{}{
+			"repository": "someImage",
+			"tag":        "1.0.0",
+		},
+		"ingress": map[string]interface{}{
+			"annotations": map[string]interface{}{
+				"traefik.frontend.rule.type":                 "PathPrefix",
+				"traefik.ingress.kubernetes.io/ssl-redirect": "true",
+			},
+		},
+	}
+
+	v.SetDefault("charts::values", values)
+
+	assert.Equal(t, "leather", v.GetString("clothing::jacket"))
+	assert.Equal(t, "01/02/03", v.GetString("emails::steve@hacker.com::created"))
+
+	type config struct {
+		Charts struct {
+			Values map[string]interface{}
+		}
+	}
+
+	expected := config{
+		Charts: struct {
+			Values map[string]interface{}
+		}{
+			Values: values,
+		},
+	}
+
+	var actual config
+
+	assert.NoError(t, v.Unmarshal(&actual))
+
+	assert.Equal(t, expected, actual)
+}
+
 func BenchmarkGetBool(b *testing.B) {
 	key := "BenchmarkGetBool"
 	v = New()