mirror of
https://github.com/spf13/viper
synced 2024-12-23 20:17:03 +00:00
Resolved merge conflicts in viper.go
This commit is contained in:
commit
319b0fc627
7 changed files with 390 additions and 58 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -22,3 +22,9 @@ _testmain.go
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.bench
|
*.bench
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# exclude dependencies in the `/vendor` folder
|
||||||
|
vendor
|
||||||
|
.idea
|
||||||
|
|
|
@ -2,8 +2,8 @@ go_import_path: github.com/spf13/viper
|
||||||
|
|
||||||
language: go
|
language: go
|
||||||
go:
|
go:
|
||||||
- 1.9.x
|
|
||||||
- 1.10.x
|
- 1.10.x
|
||||||
|
- 1.11.x
|
||||||
- tip
|
- tip
|
||||||
|
|
||||||
os:
|
os:
|
||||||
|
|
52
README.md
52
README.md
|
@ -179,19 +179,20 @@ viper.GetBool("verbose") // true
|
||||||
### Working with Environment Variables
|
### Working with Environment Variables
|
||||||
|
|
||||||
Viper has full support for environment variables. This enables 12 factor
|
Viper has full support for environment variables. This enables 12 factor
|
||||||
applications out of the box. There are four methods that exist to aid working
|
applications out of the box. There are five methods that exist to aid working
|
||||||
with ENV:
|
with ENV:
|
||||||
|
|
||||||
* `AutomaticEnv()`
|
* `AutomaticEnv()`
|
||||||
* `BindEnv(string...) : error`
|
* `BindEnv(string...) : error`
|
||||||
* `SetEnvPrefix(string)`
|
* `SetEnvPrefix(string)`
|
||||||
* `SetEnvKeyReplacer(string...) *strings.Replacer`
|
* `SetEnvKeyReplacer(string...) *strings.Replacer`
|
||||||
|
* `AllowEmptyEnvVar(bool)`
|
||||||
|
|
||||||
_When working with ENV variables, it’s important to recognize that Viper
|
_When working with ENV variables, it’s important to recognize that Viper
|
||||||
treats ENV variables as case sensitive._
|
treats ENV variables as case sensitive._
|
||||||
|
|
||||||
Viper provides a mechanism to try to ensure that ENV variables are unique. By
|
Viper provides a mechanism to try to ensure that ENV variables are unique. By
|
||||||
using `SetEnvPrefix`, you can tell Viper to use add a prefix while reading from
|
using `SetEnvPrefix`, you can tell Viper to use a prefix while reading from
|
||||||
the environment variables. Both `BindEnv` and `AutomaticEnv` will use this
|
the environment variables. Both `BindEnv` and `AutomaticEnv` will use this
|
||||||
prefix.
|
prefix.
|
||||||
|
|
||||||
|
@ -217,6 +218,10 @@ keys to an extent. This is useful if you want to use `-` or something in your
|
||||||
`Get()` calls, but want your environmental variables to use `_` delimiters. An
|
`Get()` calls, but want your environmental variables to use `_` delimiters. An
|
||||||
example of using it can be found in `viper_test.go`.
|
example of using it can be found in `viper_test.go`.
|
||||||
|
|
||||||
|
By default empty environment variables are considered unset and will fall back to
|
||||||
|
the next configuration source. To treat empty environment variables as set, use
|
||||||
|
the `AllowEmptyEnv` method.
|
||||||
|
|
||||||
#### Env example
|
#### Env example
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
@ -373,12 +378,33 @@ how to use Consul.
|
||||||
|
|
||||||
### Remote Key/Value Store Example - Unencrypted
|
### Remote Key/Value Store Example - Unencrypted
|
||||||
|
|
||||||
|
#### etcd
|
||||||
```go
|
```go
|
||||||
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
|
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
|
||||||
viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop"
|
viper.SetConfigType("json") // because there is no file extension in a stream of bytes, supported extensions are "json", "toml", "yaml", "yml", "properties", "props", "prop"
|
||||||
err := viper.ReadRemoteConfig()
|
err := viper.ReadRemoteConfig()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Consul
|
||||||
|
You need to set a key to Consul key/value storage with JSON value containing your desired config.
|
||||||
|
For example, create a Consul key/value store key `MY_CONSUL_KEY` with value:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"port": 8080,
|
||||||
|
"hostname": "myhostname.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
|
||||||
|
viper.SetConfigType("json") // Need to explicitly set this to json
|
||||||
|
err := viper.ReadRemoteConfig()
|
||||||
|
|
||||||
|
fmt.Println(viper.Get("port")) // 8080
|
||||||
|
fmt.Println(viper.Get("hostname")) // myhostname.com
|
||||||
|
```
|
||||||
|
|
||||||
### Remote Key/Value Store Example - Encrypted
|
### Remote Key/Value Store Example - Encrypted
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
@ -437,6 +463,7 @@ The following functions and methods exist:
|
||||||
* `GetTime(key string) : time.Time`
|
* `GetTime(key string) : time.Time`
|
||||||
* `GetDuration(key string) : time.Duration`
|
* `GetDuration(key string) : time.Duration`
|
||||||
* `IsSet(key string) : bool`
|
* `IsSet(key string) : bool`
|
||||||
|
* `AllSettings() : map[string]interface{}`
|
||||||
|
|
||||||
One important thing to recognize is that each Get function will return a zero
|
One important thing to recognize is that each Get function will return a zero
|
||||||
value if it’s not found. To check if a given key exists, the `IsSet()` method
|
value if it’s not found. To check if a given key exists, the `IsSet()` method
|
||||||
|
@ -590,6 +617,27 @@ if err != nil {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Marshalling to string
|
||||||
|
|
||||||
|
You may need to marhsal all the settings held in viper into a string rather than write them to a file.
|
||||||
|
You can use your favorite format's marshaller with the config returned by `AllSettings()`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
// ...
|
||||||
|
)
|
||||||
|
|
||||||
|
func yamlStringSettings() string {
|
||||||
|
c := viper.AllSettings()
|
||||||
|
bs, err := yaml.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to marshal config to YAML: %v", err)
|
||||||
|
}
|
||||||
|
return string(bs)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Viper or Vipers?
|
## Viper or Vipers?
|
||||||
|
|
||||||
Viper comes ready to use out of the box. There is no configuration or
|
Viper comes ready to use out of the box. There is no configuration or
|
||||||
|
|
16
go.mod
Normal file
16
go.mod
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module github.com/spf13/viper
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7
|
||||||
|
github.com/hashicorp/hcl v1.0.0
|
||||||
|
github.com/magiconair/properties v1.8.0
|
||||||
|
github.com/mitchellh/mapstructure v1.0.0
|
||||||
|
github.com/pelletier/go-toml v1.2.0
|
||||||
|
github.com/spf13/afero v1.1.2
|
||||||
|
github.com/spf13/cast v1.2.0
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0
|
||||||
|
github.com/spf13/pflag v1.0.2
|
||||||
|
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 // indirect
|
||||||
|
golang.org/x/text v0.3.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.2.1
|
||||||
|
)
|
26
go.sum
Normal file
26
go.sum
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||||
|
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||||
|
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
|
||||||
|
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||||
|
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||||
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||||
|
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||||
|
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
|
||||||
|
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||||
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
|
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
|
||||||
|
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
|
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
|
||||||
|
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
126
viper.go
126
viper.go
|
@ -30,6 +30,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
@ -113,6 +114,23 @@ func (fnfe ConfigFileNotFoundError) Error() string {
|
||||||
return fmt.Sprintf("Config File %q Not Found in %q", fnfe.name, fnfe.locations)
|
return fmt.Sprintf("Config File %q Not Found in %q", fnfe.name, fnfe.locations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A DecoderConfigOption can be passed to viper.Unmarshal to configure
|
||||||
|
// mapstructure.DecoderConfig options
|
||||||
|
type DecoderConfigOption func(*mapstructure.DecoderConfig)
|
||||||
|
|
||||||
|
// DecodeHook returns a DecoderConfigOption which overrides the default
|
||||||
|
// DecoderConfig.DecodeHook value, the default is:
|
||||||
|
//
|
||||||
|
// mapstructure.ComposeDecodeHookFunc(
|
||||||
|
// mapstructure.StringToTimeDurationHookFunc(),
|
||||||
|
// mapstructure.StringToSliceHookFunc(","),
|
||||||
|
// )
|
||||||
|
func DecodeHook(hook mapstructure.DecodeHookFunc) DecoderConfigOption {
|
||||||
|
return func(c *mapstructure.DecoderConfig) {
|
||||||
|
c.DecodeHook = hook
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Viper is a prioritized configuration registry. It
|
// Viper is a prioritized configuration registry. It
|
||||||
// maintains a set of configuration sources, fetches
|
// maintains a set of configuration sources, fetches
|
||||||
// values to populate those, and provides them according
|
// values to populate those, and provides them according
|
||||||
|
@ -169,6 +187,7 @@ type Viper struct {
|
||||||
|
|
||||||
automaticEnvApplied bool
|
automaticEnvApplied bool
|
||||||
envKeyReplacer *strings.Replacer
|
envKeyReplacer *strings.Replacer
|
||||||
|
allowEmptyEnv bool
|
||||||
|
|
||||||
config map[string]interface{}
|
config map[string]interface{}
|
||||||
override map[string]interface{}
|
override map[string]interface{}
|
||||||
|
@ -259,50 +278,73 @@ func (v *Viper) OnConfigChange(run func(in fsnotify.Event)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func WatchConfig() { v.WatchConfig() }
|
func WatchConfig() { v.WatchConfig() }
|
||||||
|
|
||||||
func (v *Viper) WatchConfig() {
|
func (v *Viper) WatchConfig() {
|
||||||
|
initWG := sync.WaitGroup{}
|
||||||
|
initWG.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
watcher, err := fsnotify.NewWatcher()
|
watcher, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
defer watcher.Close()
|
defer watcher.Close()
|
||||||
|
|
||||||
// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
|
// we have to watch the entire directory to pick up renames/atomic saves in a cross-platform way
|
||||||
filename, err := v.getConfigFile()
|
filename, err := v.getConfigFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error:", err)
|
log.Printf("error: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
configFile := filepath.Clean(filename)
|
configFile := filepath.Clean(filename)
|
||||||
configDir, _ := filepath.Split(configFile)
|
configDir, _ := filepath.Split(configFile)
|
||||||
|
realConfigFile, _ := filepath.EvalSymlinks(filename)
|
||||||
|
|
||||||
done := make(chan bool)
|
eventsWG := sync.WaitGroup{}
|
||||||
|
eventsWG.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case event := <-watcher.Events:
|
case event, ok := <-watcher.Events:
|
||||||
// we only care about the config file
|
if !ok { // 'Events' channel is closed
|
||||||
if filepath.Clean(event.Name) == configFile {
|
eventsWG.Done()
|
||||||
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
return
|
||||||
|
}
|
||||||
|
currentConfigFile, _ := filepath.EvalSymlinks(filename)
|
||||||
|
// we only care about the config file with the following cases:
|
||||||
|
// 1 - if the config file was modified or created
|
||||||
|
// 2 - if the real path to the config file changed (eg: k8s ConfigMap replacement)
|
||||||
|
const writeOrCreateMask = fsnotify.Write | fsnotify.Create
|
||||||
|
if (filepath.Clean(event.Name) == configFile &&
|
||||||
|
event.Op&writeOrCreateMask != 0) ||
|
||||||
|
(currentConfigFile != "" && currentConfigFile != realConfigFile) {
|
||||||
|
realConfigFile = currentConfigFile
|
||||||
err := v.ReadInConfig()
|
err := v.ReadInConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error:", err)
|
log.Printf("error reading config file: %v\n", err)
|
||||||
}
|
}
|
||||||
if v.onConfigChange != nil {
|
if v.onConfigChange != nil {
|
||||||
v.onConfigChange(event)
|
v.onConfigChange(event)
|
||||||
}
|
}
|
||||||
|
} else if filepath.Clean(event.Name) == configFile &&
|
||||||
|
event.Op&fsnotify.Remove&fsnotify.Remove != 0 {
|
||||||
|
eventsWG.Done()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case err := <-watcher.Errors:
|
|
||||||
log.Println("error:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
watcher.Add(configDir)
|
case err, ok := <-watcher.Errors:
|
||||||
<-done
|
if ok { // 'Errors' channel is not closed
|
||||||
|
log.Printf("watcher error: %v\n", err)
|
||||||
|
}
|
||||||
|
eventsWG.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
watcher.Add(configDir)
|
||||||
|
initWG.Done() // done initalizing the watch in this go routine, so the parent routine can move on...
|
||||||
|
eventsWG.Wait() // now, wait for event loop to end in this go-routine...
|
||||||
|
}()
|
||||||
|
initWG.Wait() // make sure that the go routine above fully ended before returning
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfigFile explicitly defines the path, name and extension of the config file.
|
// SetConfigFile explicitly defines the path, name and extension of the config file.
|
||||||
|
@ -332,6 +374,14 @@ func (v *Viper) mergeWithEnvPrefix(in string) string {
|
||||||
return strings.ToUpper(in)
|
return strings.ToUpper(in)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllowEmptyEnv tells Viper to consider set,
|
||||||
|
// but empty environment variables as valid values instead of falling back.
|
||||||
|
// For backward compatibility reasons this is false by default.
|
||||||
|
func AllowEmptyEnv(allowEmptyEnv bool) { v.AllowEmptyEnv(allowEmptyEnv) }
|
||||||
|
func (v *Viper) AllowEmptyEnv(allowEmptyEnv bool) {
|
||||||
|
v.allowEmptyEnv = allowEmptyEnv
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: should getEnv logic be moved into find(). Can generalize the use of
|
// TODO: should getEnv logic be moved into find(). Can generalize the use of
|
||||||
// rewriting keys many things, Ex: Get('someKey') -> some_key
|
// rewriting keys many things, Ex: Get('someKey') -> some_key
|
||||||
// (camel case to snake case for JSON keys perhaps)
|
// (camel case to snake case for JSON keys perhaps)
|
||||||
|
@ -339,11 +389,14 @@ func (v *Viper) mergeWithEnvPrefix(in string) string {
|
||||||
// getEnv is a wrapper around os.Getenv which replaces characters in the original
|
// getEnv is a wrapper around os.Getenv which replaces characters in the original
|
||||||
// key. This allows env vars which have different keys than the config object
|
// key. This allows env vars which have different keys than the config object
|
||||||
// keys.
|
// keys.
|
||||||
func (v *Viper) getEnv(key string) string {
|
func (v *Viper) getEnv(key string) (string, bool) {
|
||||||
if v.envKeyReplacer != nil {
|
if v.envKeyReplacer != nil {
|
||||||
key = v.envKeyReplacer.Replace(key)
|
key = v.envKeyReplacer.Replace(key)
|
||||||
}
|
}
|
||||||
return os.Getenv(key)
|
|
||||||
|
val, ok := os.LookupEnv(key)
|
||||||
|
|
||||||
|
return val, ok && (v.allowEmptyEnv || val != "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigFileUsed returns the file used to populate the config registry.
|
// ConfigFileUsed returns the file used to populate the config registry.
|
||||||
|
@ -570,10 +623,9 @@ func (v *Viper) isPathShadowedInFlatMap(path []string, mi interface{}) string {
|
||||||
// "foo.bar.baz" in a lower-priority map
|
// "foo.bar.baz" in a lower-priority map
|
||||||
func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
|
func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
|
||||||
var parentKey string
|
var parentKey string
|
||||||
var val string
|
|
||||||
for i := 1; i < len(path); i++ {
|
for i := 1; i < len(path); i++ {
|
||||||
parentKey = strings.Join(path[0:i], v.keyDelim)
|
parentKey = strings.Join(path[0:i], v.keyDelim)
|
||||||
if val = v.getEnv(v.mergeWithEnvPrefix(parentKey)); val != "" {
|
if _, ok := v.getEnv(v.mergeWithEnvPrefix(parentKey)); ok {
|
||||||
return parentKey
|
return parentKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -633,8 +685,10 @@ func (v *Viper) Get(key string) interface{} {
|
||||||
return cast.ToBool(val)
|
return cast.ToBool(val)
|
||||||
case string:
|
case string:
|
||||||
return cast.ToString(val)
|
return cast.ToString(val)
|
||||||
case int64, int32, int16, int8, int:
|
case int32, int16, int8, int:
|
||||||
return cast.ToInt(val)
|
return cast.ToInt(val)
|
||||||
|
case int64:
|
||||||
|
return cast.ToInt64(val)
|
||||||
case float64, float32:
|
case float64, float32:
|
||||||
return cast.ToFloat64(val)
|
return cast.ToFloat64(val)
|
||||||
case time.Time:
|
case time.Time:
|
||||||
|
@ -747,9 +801,11 @@ func (v *Viper) GetSizeInBytes(key string) uint {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalKey takes a single key and unmarshals it into a Struct.
|
// UnmarshalKey takes a single key and unmarshals it into a Struct.
|
||||||
func UnmarshalKey(key string, rawVal interface{}) error { return v.UnmarshalKey(key, rawVal) }
|
func UnmarshalKey(key string, rawVal interface{}, opts ...DecoderConfigOption) error {
|
||||||
func (v *Viper) UnmarshalKey(key string, rawVal interface{}) error {
|
return v.UnmarshalKey(key, rawVal, opts...)
|
||||||
err := decode(v.Get(key), defaultDecoderConfig(rawVal))
|
}
|
||||||
|
func (v *Viper) UnmarshalKey(key string, rawVal interface{}, opts ...DecoderConfigOption) error {
|
||||||
|
err := decode(v.Get(key), defaultDecoderConfig(rawVal, opts...))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -762,9 +818,11 @@ func (v *Viper) UnmarshalKey(key string, rawVal interface{}) error {
|
||||||
|
|
||||||
// Unmarshal unmarshals the config into a Struct. Make sure that the tags
|
// Unmarshal unmarshals the config into a Struct. Make sure that the tags
|
||||||
// on the fields of the structure are properly set.
|
// on the fields of the structure are properly set.
|
||||||
func Unmarshal(rawVal interface{}) error { return v.Unmarshal(rawVal) }
|
func Unmarshal(rawVal interface{}, opts ...DecoderConfigOption) error {
|
||||||
func (v *Viper) Unmarshal(rawVal interface{}) error {
|
return v.Unmarshal(rawVal, opts...)
|
||||||
err := decode(v.AllSettings(), defaultDecoderConfig(rawVal))
|
}
|
||||||
|
func (v *Viper) Unmarshal(rawVal interface{}, opts ...DecoderConfigOption) error {
|
||||||
|
err := decode(v.AllSettings(), defaultDecoderConfig(rawVal, opts...))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -777,8 +835,8 @@ func (v *Viper) Unmarshal(rawVal interface{}) error {
|
||||||
|
|
||||||
// defaultDecoderConfig returns default mapsstructure.DecoderConfig with suppot
|
// defaultDecoderConfig returns default mapsstructure.DecoderConfig with suppot
|
||||||
// of time.Duration values & string slices
|
// of time.Duration values & string slices
|
||||||
func defaultDecoderConfig(output interface{}) *mapstructure.DecoderConfig {
|
func defaultDecoderConfig(output interface{}, opts ...DecoderConfigOption) *mapstructure.DecoderConfig {
|
||||||
return &mapstructure.DecoderConfig{
|
c := &mapstructure.DecoderConfig{
|
||||||
Metadata: nil,
|
Metadata: nil,
|
||||||
Result: output,
|
Result: output,
|
||||||
WeaklyTypedInput: true,
|
WeaklyTypedInput: true,
|
||||||
|
@ -787,6 +845,10 @@ func defaultDecoderConfig(output interface{}) *mapstructure.DecoderConfig {
|
||||||
mapstructure.StringToSliceHookFunc(","),
|
mapstructure.StringToSliceHookFunc(","),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(c)
|
||||||
|
}
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// A wrapper around mapstructure.Decode that mimics the WeakDecode functionality
|
// A wrapper around mapstructure.Decode that mimics the WeakDecode functionality
|
||||||
|
@ -942,7 +1004,7 @@ func (v *Viper) find(lcaseKey string) interface{} {
|
||||||
if v.automaticEnvApplied {
|
if v.automaticEnvApplied {
|
||||||
// even if it hasn't been registered, if automaticEnv is used,
|
// even if it hasn't been registered, if automaticEnv is used,
|
||||||
// check any Get request
|
// check any Get request
|
||||||
if val = v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); val != "" {
|
if val, ok := v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); ok {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
if nested && v.isPathShadowedInAutoEnv(path) != "" {
|
if nested && v.isPathShadowedInAutoEnv(path) != "" {
|
||||||
|
@ -951,7 +1013,7 @@ func (v *Viper) find(lcaseKey string) interface{} {
|
||||||
}
|
}
|
||||||
envkey, exists := v.env[lcaseKey]
|
envkey, exists := v.env[lcaseKey]
|
||||||
if exists {
|
if exists {
|
||||||
if val = v.getEnv(envkey); val != "" {
|
if val, ok := v.getEnv(envkey); ok {
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1116,7 +1178,7 @@ func (v *Viper) SetDefault(key string, value interface{}) {
|
||||||
deepestMap[lastKey] = value
|
deepestMap[lastKey] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sets the value for the key in the override regiser.
|
// Set sets the value for the key in the override register.
|
||||||
// Set is case-insensitive for a key.
|
// Set is case-insensitive for a key.
|
||||||
// Will be used instead of values obtained via
|
// Will be used instead of values obtained via
|
||||||
// flags, config file, ENV, default, or key/value store.
|
// flags, config file, ENV, default, or key/value store.
|
||||||
|
|
204
viper_test.go
204
viper_test.go
|
@ -7,22 +7,29 @@ package viper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
var yamlExample = []byte(`Hacker: true
|
var yamlExample = []byte(`Hacker: true
|
||||||
|
@ -381,6 +388,36 @@ func TestEnv(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEmptyEnv(t *testing.T) {
|
||||||
|
initJSON()
|
||||||
|
|
||||||
|
BindEnv("type") // Empty environment variable
|
||||||
|
BindEnv("name") // Bound, but not set environment variable
|
||||||
|
|
||||||
|
os.Clearenv()
|
||||||
|
|
||||||
|
os.Setenv("TYPE", "")
|
||||||
|
|
||||||
|
assert.Equal(t, "donut", Get("type"))
|
||||||
|
assert.Equal(t, "Cake", Get("name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyEnv_Allowed(t *testing.T) {
|
||||||
|
initJSON()
|
||||||
|
|
||||||
|
AllowEmptyEnv(true)
|
||||||
|
|
||||||
|
BindEnv("type") // Empty environment variable
|
||||||
|
BindEnv("name") // Bound, but not set environment variable
|
||||||
|
|
||||||
|
os.Clearenv()
|
||||||
|
|
||||||
|
os.Setenv("TYPE", "")
|
||||||
|
|
||||||
|
assert.Equal(t, "", Get("type"))
|
||||||
|
assert.Equal(t, "Cake", Get("name"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnvPrefix(t *testing.T) {
|
func TestEnvPrefix(t *testing.T) {
|
||||||
initJSON()
|
initJSON()
|
||||||
|
|
||||||
|
@ -503,6 +540,42 @@ func TestUnmarshal(t *testing.T) {
|
||||||
assert.Equal(t, &config{Name: "Steve", Port: 1234, Duration: time.Second + time.Millisecond}, &C)
|
assert.Equal(t, &config{Name: "Steve", Port: 1234, Duration: time.Second + time.Millisecond}, &C)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalWithDecoderOptions(t *testing.T) {
|
||||||
|
Set("credentials", "{\"foo\":\"bar\"}")
|
||||||
|
|
||||||
|
opt := DecodeHook(mapstructure.ComposeDecodeHookFunc(
|
||||||
|
mapstructure.StringToTimeDurationHookFunc(),
|
||||||
|
mapstructure.StringToSliceHookFunc(","),
|
||||||
|
// Custom Decode Hook Function
|
||||||
|
func(rf reflect.Kind, rt reflect.Kind, data interface{}) (interface{}, error) {
|
||||||
|
if rf != reflect.String || rt != reflect.Map {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
m := map[string]string{}
|
||||||
|
raw := data.(string)
|
||||||
|
if raw == "" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, json.Unmarshal([]byte(raw), &m)
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Credentials map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var C config
|
||||||
|
|
||||||
|
err := Unmarshal(&C, opt)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to decode into struct, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, &config{
|
||||||
|
Credentials: map[string]string{"foo": "bar"},
|
||||||
|
}, &C)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindPFlags(t *testing.T) {
|
func TestBindPFlags(t *testing.T) {
|
||||||
v := New() // create independent Viper object
|
v := New() // create independent Viper object
|
||||||
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||||
|
@ -540,24 +613,28 @@ func TestBindPFlags(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBindPFlagsStringSlice(t *testing.T) {
|
func TestBindPFlagsStringSlice(t *testing.T) {
|
||||||
for _, testValue := range []struct {
|
tests := []struct {
|
||||||
Expected []string
|
Expected []string
|
||||||
Value string
|
Value string
|
||||||
}{
|
}{
|
||||||
{[]string{}, ""},
|
{[]string{}, ""},
|
||||||
{[]string{"jeden"}, "jeden"},
|
{[]string{"jeden"}, "jeden"},
|
||||||
{[]string{"dwa", "trzy"}, "dwa,trzy"},
|
{[]string{"dwa", "trzy"}, "dwa,trzy"},
|
||||||
{[]string{"cztery", "piec , szesc"}, "cztery,\"piec , szesc\""}} {
|
{[]string{"cztery", "piec , szesc"}, "cztery,\"piec , szesc\""},
|
||||||
|
}
|
||||||
|
|
||||||
for _, changed := range []bool{true, false} {
|
|
||||||
v := New() // create independent Viper object
|
v := New() // create independent Viper object
|
||||||
|
defaultVal := []string{"default"}
|
||||||
|
v.SetDefault("stringslice", defaultVal)
|
||||||
|
|
||||||
|
for _, testValue := range tests {
|
||||||
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||||
flagSet.StringSlice("stringslice", testValue.Expected, "test")
|
flagSet.StringSlice("stringslice", testValue.Expected, "test")
|
||||||
flagSet.Visit(func(f *pflag.Flag) {
|
|
||||||
if len(testValue.Value) > 0 {
|
for _, changed := range []bool{true, false} {
|
||||||
|
flagSet.VisitAll(func(f *pflag.Flag) {
|
||||||
f.Value.Set(testValue.Value)
|
f.Value.Set(testValue.Value)
|
||||||
f.Changed = changed
|
f.Changed = changed
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
err := v.BindPFlags(flagSet)
|
err := v.BindPFlags(flagSet)
|
||||||
|
@ -572,7 +649,11 @@ func TestBindPFlagsStringSlice(t *testing.T) {
|
||||||
if err := v.Unmarshal(val); err != nil {
|
if err := v.Unmarshal(val); err != nil {
|
||||||
t.Fatalf("%+#v cannot unmarshal: %s", testValue.Value, err)
|
t.Fatalf("%+#v cannot unmarshal: %s", testValue.Value, err)
|
||||||
}
|
}
|
||||||
assert.Equal(t, testValue.Expected, val.StringSlice)
|
if changed {
|
||||||
|
assert.EqualValues(t, testValue.Expected, val.StringSlice)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, defaultVal, val.StringSlice)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1064,10 +1145,6 @@ func TestMergeConfig(t *testing.T) {
|
||||||
t.Fatalf("pop != 37890, = %d", pop)
|
t.Fatalf("pop != 37890, = %d", pop)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pop := v.GetInt("hello.lagrenum"); pop != 765432101234567 {
|
|
||||||
t.Fatalf("lagrenum != 765432101234567, = %d", pop)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pop := v.GetInt32("hello.pop"); pop != int32(37890) {
|
if pop := v.GetInt32("hello.pop"); pop != int32(37890) {
|
||||||
t.Fatalf("pop != 37890, = %d", pop)
|
t.Fatalf("pop != 37890, = %d", pop)
|
||||||
}
|
}
|
||||||
|
@ -1092,10 +1169,6 @@ func TestMergeConfig(t *testing.T) {
|
||||||
t.Fatalf("pop != 45000, = %d", pop)
|
t.Fatalf("pop != 45000, = %d", pop)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pop := v.GetInt("hello.lagrenum"); pop != 7654321001234567 {
|
|
||||||
t.Fatalf("lagrenum != 7654321001234567, = %d", pop)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pop := v.GetInt32("hello.pop"); pop != int32(45000) {
|
if pop := v.GetInt32("hello.pop"); pop != int32(45000) {
|
||||||
t.Fatalf("pop != 45000, = %d", pop)
|
t.Fatalf("pop != 45000, = %d", pop)
|
||||||
}
|
}
|
||||||
|
@ -1376,6 +1449,107 @@ func doTestCaseInsensitive(t *testing.T, typ, config string) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newViperWithConfigFile(t *testing.T) (*Viper, string, func()) {
|
||||||
|
watchDir, err := ioutil.TempDir("", "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
configFile := path.Join(watchDir, "config.yaml")
|
||||||
|
err = ioutil.WriteFile(configFile, []byte("foo: bar\n"), 0640)
|
||||||
|
require.Nil(t, err)
|
||||||
|
cleanup := func() {
|
||||||
|
os.RemoveAll(watchDir)
|
||||||
|
}
|
||||||
|
v := New()
|
||||||
|
v.SetConfigFile(configFile)
|
||||||
|
err = v.ReadInConfig()
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "bar", v.Get("foo"))
|
||||||
|
return v, configFile, cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
func newViperWithSymlinkedConfigFile(t *testing.T) (*Viper, string, string, func()) {
|
||||||
|
watchDir, err := ioutil.TempDir("", "")
|
||||||
|
require.Nil(t, err)
|
||||||
|
dataDir1 := path.Join(watchDir, "data1")
|
||||||
|
err = os.Mkdir(dataDir1, 0777)
|
||||||
|
require.Nil(t, err)
|
||||||
|
realConfigFile := path.Join(dataDir1, "config.yaml")
|
||||||
|
t.Logf("Real config file location: %s\n", realConfigFile)
|
||||||
|
err = ioutil.WriteFile(realConfigFile, []byte("foo: bar\n"), 0640)
|
||||||
|
require.Nil(t, err)
|
||||||
|
cleanup := func() {
|
||||||
|
os.RemoveAll(watchDir)
|
||||||
|
}
|
||||||
|
// now, symlink the tm `data1` dir to `data` in the baseDir
|
||||||
|
os.Symlink(dataDir1, path.Join(watchDir, "data"))
|
||||||
|
// and link the `<watchdir>/datadir1/config.yaml` to `<watchdir>/config.yaml`
|
||||||
|
configFile := path.Join(watchDir, "config.yaml")
|
||||||
|
os.Symlink(path.Join(watchDir, "data", "config.yaml"), configFile)
|
||||||
|
t.Logf("Config file location: %s\n", path.Join(watchDir, "config.yaml"))
|
||||||
|
// init Viper
|
||||||
|
v := New()
|
||||||
|
v.SetConfigFile(configFile)
|
||||||
|
err = v.ReadInConfig()
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "bar", v.Get("foo"))
|
||||||
|
return v, watchDir, configFile, cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatchFile(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("file content changed", func(t *testing.T) {
|
||||||
|
// given a `config.yaml` file being watched
|
||||||
|
v, configFile, cleanup := newViperWithConfigFile(t)
|
||||||
|
defer cleanup()
|
||||||
|
_, err := os.Stat(configFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("test config file: %s\n", configFile)
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
t.Logf("config file changed")
|
||||||
|
wg.Done()
|
||||||
|
})
|
||||||
|
v.WatchConfig()
|
||||||
|
// when overwriting the file and waiting for the custom change notification handler to be triggered
|
||||||
|
err = ioutil.WriteFile(configFile, []byte("foo: baz\n"), 0640)
|
||||||
|
wg.Wait()
|
||||||
|
// then the config value should have changed
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, "baz", v.Get("foo"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("link to real file changed (à la Kubernetes)", func(t *testing.T) {
|
||||||
|
// skip if not executed on Linux
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
t.Skipf("Skipping test as symlink replacements don't work on non-linux environment...")
|
||||||
|
}
|
||||||
|
v, watchDir, _, _ := newViperWithSymlinkedConfigFile(t)
|
||||||
|
// defer cleanup()
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
v.WatchConfig()
|
||||||
|
v.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
t.Logf("config file changed")
|
||||||
|
wg.Done()
|
||||||
|
})
|
||||||
|
wg.Add(1)
|
||||||
|
// when link to another `config.yaml` file
|
||||||
|
dataDir2 := path.Join(watchDir, "data2")
|
||||||
|
err := os.Mkdir(dataDir2, 0777)
|
||||||
|
require.Nil(t, err)
|
||||||
|
configFile2 := path.Join(dataDir2, "config.yaml")
|
||||||
|
err = ioutil.WriteFile(configFile2, []byte("foo: baz\n"), 0640)
|
||||||
|
require.Nil(t, err)
|
||||||
|
// change the symlink using the `ln -sfn` command
|
||||||
|
err = exec.Command("ln", "-sfn", dataDir2, path.Join(watchDir, "data")).Run()
|
||||||
|
require.Nil(t, err)
|
||||||
|
wg.Wait()
|
||||||
|
// then
|
||||||
|
require.Nil(t, err)
|
||||||
|
assert.Equal(t, "baz", v.Get("foo"))
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkGetBool(b *testing.B) {
|
func BenchmarkGetBool(b *testing.B) {
|
||||||
key := "BenchmarkGetBool"
|
key := "BenchmarkGetBool"
|
||||||
v = New()
|
v = New()
|
||||||
|
|
Loading…
Reference in a new issue