mirror of https://github.com/spf13/viper.git
Support `.env` format files (#528)
* Support `.env` format files * Missing "dotenv" from SupportedExtns
This commit is contained in:
parent
b5bf975e58
commit
3620d3d9e1
26
viper.go
26
viper.go
|
@ -45,6 +45,7 @@ import (
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
jww "github.com/spf13/jwalterweatherman"
|
jww "github.com/spf13/jwalterweatherman"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
"github.com/subosito/gotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigMarshalError happens when failing to marshal the configuration.
|
// ConfigMarshalError happens when failing to marshal the configuration.
|
||||||
|
@ -230,7 +231,7 @@ func New() *Viper {
|
||||||
// can use it in their testing as well.
|
// can use it in their testing as well.
|
||||||
func Reset() {
|
func Reset() {
|
||||||
v = New()
|
v = New()
|
||||||
SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl"}
|
SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "dotenv", "env"}
|
||||||
SupportedRemoteProviders = []string{"etcd", "consul"}
|
SupportedRemoteProviders = []string{"etcd", "consul"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,7 +270,7 @@ type RemoteProvider interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupportedExts are universally supported extensions.
|
// SupportedExts are universally supported extensions.
|
||||||
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl"}
|
var SupportedExts = []string{"json", "toml", "yaml", "yml", "properties", "props", "prop", "hcl", "dotenv", "env"}
|
||||||
|
|
||||||
// SupportedRemoteProviders are universally supported remote providers.
|
// SupportedRemoteProviders are universally supported remote providers.
|
||||||
var SupportedRemoteProviders = []string{"etcd", "consul"}
|
var SupportedRemoteProviders = []string{"etcd", "consul"}
|
||||||
|
@ -1400,6 +1401,15 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error {
|
||||||
c[k] = v
|
c[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "dotenv", "env":
|
||||||
|
env, err := gotenv.StrictParse(buf)
|
||||||
|
if err != nil {
|
||||||
|
return ConfigParseError{err}
|
||||||
|
}
|
||||||
|
for k, v := range env {
|
||||||
|
c[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
case "properties", "props", "prop":
|
case "properties", "props", "prop":
|
||||||
v.properties = properties.NewProperties()
|
v.properties = properties.NewProperties()
|
||||||
var err error
|
var err error
|
||||||
|
@ -1465,6 +1475,18 @@ func (v *Viper) marshalWriter(f afero.File, configType string) error {
|
||||||
return ConfigMarshalError{err}
|
return ConfigMarshalError{err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "dotenv", "env":
|
||||||
|
lines := []string{}
|
||||||
|
for _, key := range v.AllKeys() {
|
||||||
|
envName := strings.ToUpper(strings.Replace(key, ".", "_", -1))
|
||||||
|
val := v.Get(key)
|
||||||
|
lines = append(lines, fmt.Sprintf("%v=%v", envName, val))
|
||||||
|
}
|
||||||
|
s := strings.Join(lines, "\n")
|
||||||
|
if _, err := f.WriteString(s); err != nil {
|
||||||
|
return ConfigMarshalError{err}
|
||||||
|
}
|
||||||
|
|
||||||
case "toml":
|
case "toml":
|
||||||
t, err := toml.TreeFromMap(c)
|
t, err := toml.TreeFromMap(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -64,6 +64,11 @@ organization = "MongoDB"
|
||||||
Bio = "MongoDB Chief Developer Advocate & Hacker at Large"
|
Bio = "MongoDB Chief Developer Advocate & Hacker at Large"
|
||||||
dob = 1979-05-27T07:32:00Z # First class dates? Why not?`)
|
dob = 1979-05-27T07:32:00Z # First class dates? Why not?`)
|
||||||
|
|
||||||
|
var dotenvExample = []byte(`
|
||||||
|
TITLE_DOTENV="DotEnv Example"
|
||||||
|
TYPE_DOTENV=donut
|
||||||
|
NAME_DOTENV=Cake`)
|
||||||
|
|
||||||
var jsonExample = []byte(`{
|
var jsonExample = []byte(`{
|
||||||
"id": "0001",
|
"id": "0001",
|
||||||
"type": "donut",
|
"type": "donut",
|
||||||
|
@ -136,6 +141,10 @@ func initConfigs() {
|
||||||
r = bytes.NewReader(tomlExample)
|
r = bytes.NewReader(tomlExample)
|
||||||
unmarshalReader(r, v.config)
|
unmarshalReader(r, v.config)
|
||||||
|
|
||||||
|
SetConfigType("env")
|
||||||
|
r = bytes.NewReader(dotenvExample)
|
||||||
|
unmarshalReader(r, v.config)
|
||||||
|
|
||||||
SetConfigType("json")
|
SetConfigType("json")
|
||||||
remote := bytes.NewReader(remoteExample)
|
remote := bytes.NewReader(remoteExample)
|
||||||
unmarshalReader(remote, v.kvstore)
|
unmarshalReader(remote, v.kvstore)
|
||||||
|
@ -179,6 +188,14 @@ func initTOML() {
|
||||||
unmarshalReader(r, v.config)
|
unmarshalReader(r, v.config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initDotEnv() {
|
||||||
|
Reset()
|
||||||
|
SetConfigType("env")
|
||||||
|
r := bytes.NewReader(dotenvExample)
|
||||||
|
|
||||||
|
unmarshalReader(r, v.config)
|
||||||
|
}
|
||||||
|
|
||||||
func initHcl() {
|
func initHcl() {
|
||||||
Reset()
|
Reset()
|
||||||
SetConfigType("hcl")
|
SetConfigType("hcl")
|
||||||
|
@ -342,6 +359,11 @@ func TestTOML(t *testing.T) {
|
||||||
assert.Equal(t, "TOML Example", Get("title"))
|
assert.Equal(t, "TOML Example", Get("title"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDotEnv(t *testing.T) {
|
||||||
|
initDotEnv()
|
||||||
|
assert.Equal(t, "DotEnv Example", Get("title_dotenv"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestHCL(t *testing.T) {
|
func TestHCL(t *testing.T) {
|
||||||
initHcl()
|
initHcl()
|
||||||
assert.Equal(t, "0001", Get("id"))
|
assert.Equal(t, "0001", Get("id"))
|
||||||
|
@ -470,9 +492,11 @@ func TestSetEnvKeyReplacer(t *testing.T) {
|
||||||
func TestAllKeys(t *testing.T) {
|
func TestAllKeys(t *testing.T) {
|
||||||
initConfigs()
|
initConfigs()
|
||||||
|
|
||||||
ks := sort.StringSlice{"title", "newkey", "owner.organization", "owner.dob", "owner.bio", "name", "beard", "ppu", "batters.batter", "hobbies", "clothing.jacket", "clothing.trousers", "clothing.pants.size", "age", "hacker", "id", "type", "eyes", "p_id", "p_ppu", "p_batters.batter.type", "p_type", "p_name", "foos"}
|
ks := sort.StringSlice{"title", "newkey", "owner.organization", "owner.dob", "owner.bio", "name", "beard", "ppu", "batters.batter", "hobbies", "clothing.jacket", "clothing.trousers", "clothing.pants.size", "age", "hacker", "id", "type", "eyes", "p_id", "p_ppu", "p_batters.batter.type", "p_type", "p_name", "foos",
|
||||||
|
"title_dotenv", "type_dotenv", "name_dotenv",
|
||||||
|
}
|
||||||
dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
|
dob, _ := time.Parse(time.RFC3339, "1979-05-27T07:32:00Z")
|
||||||
all := map[string]interface{}{"owner": map[string]interface{}{"organization": "MongoDB", "bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob}, "title": "TOML Example", "ppu": 0.55, "eyes": "brown", "clothing": map[string]interface{}{"trousers": "denim", "jacket": "leather", "pants": map[string]interface{}{"size": "large"}}, "id": "0001", "batters": map[string]interface{}{"batter": []interface{}{map[string]interface{}{"type": "Regular"}, map[string]interface{}{"type": "Chocolate"}, map[string]interface{}{"type": "Blueberry"}, map[string]interface{}{"type": "Devil's Food"}}}, "hacker": true, "beard": true, "hobbies": []interface{}{"skateboarding", "snowboarding", "go"}, "age": 35, "type": "donut", "newkey": "remote", "name": "Cake", "p_id": "0001", "p_ppu": "0.55", "p_name": "Cake", "p_batters": map[string]interface{}{"batter": map[string]interface{}{"type": "Regular"}}, "p_type": "donut", "foos": []map[string]interface{}{map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"key": 1}, map[string]interface{}{"key": 2}, map[string]interface{}{"key": 3}, map[string]interface{}{"key": 4}}}}}
|
all := map[string]interface{}{"owner": map[string]interface{}{"organization": "MongoDB", "bio": "MongoDB Chief Developer Advocate & Hacker at Large", "dob": dob}, "title": "TOML Example", "ppu": 0.55, "eyes": "brown", "clothing": map[string]interface{}{"trousers": "denim", "jacket": "leather", "pants": map[string]interface{}{"size": "large"}}, "id": "0001", "batters": map[string]interface{}{"batter": []interface{}{map[string]interface{}{"type": "Regular"}, map[string]interface{}{"type": "Chocolate"}, map[string]interface{}{"type": "Blueberry"}, map[string]interface{}{"type": "Devil's Food"}}}, "hacker": true, "beard": true, "hobbies": []interface{}{"skateboarding", "snowboarding", "go"}, "age": 35, "type": "donut", "newkey": "remote", "name": "Cake", "p_id": "0001", "p_ppu": "0.55", "p_name": "Cake", "p_batters": map[string]interface{}{"batter": map[string]interface{}{"type": "Regular"}}, "p_type": "donut", "foos": []map[string]interface{}{map[string]interface{}{"foo": []map[string]interface{}{map[string]interface{}{"key": 1}, map[string]interface{}{"key": 2}, map[string]interface{}{"key": 3}, map[string]interface{}{"key": 4}}}}, "title_dotenv": "DotEnv Example", "type_dotenv": "donut", "name_dotenv": "Cake"}
|
||||||
|
|
||||||
var allkeys sort.StringSlice
|
var allkeys sort.StringSlice
|
||||||
allkeys = AllKeys()
|
allkeys = AllKeys()
|
||||||
|
@ -756,6 +780,9 @@ func TestFindsNestedKeys(t *testing.T) {
|
||||||
"hobbies": []interface{}{
|
"hobbies": []interface{}{
|
||||||
"skateboarding", "snowboarding", "go",
|
"skateboarding", "snowboarding", "go",
|
||||||
},
|
},
|
||||||
|
"TITLE_DOTENV": "DotEnv Example",
|
||||||
|
"TYPE_DOTENV": "donut",
|
||||||
|
"NAME_DOTENV": "Cake",
|
||||||
"title": "TOML Example",
|
"title": "TOML Example",
|
||||||
"newkey": "remote",
|
"newkey": "remote",
|
||||||
"batters": map[string]interface{}{
|
"batters": map[string]interface{}{
|
||||||
|
@ -1077,6 +1104,43 @@ func TestWriteConfigTOML(t *testing.T) {
|
||||||
assert.Equal(t, v.GetString("owner.organization"), v2.GetString("owner.organization"))
|
assert.Equal(t, v.GetString("owner.organization"), v2.GetString("owner.organization"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var dotenvWriteExpected = []byte(`
|
||||||
|
TITLE="DotEnv Write Example"
|
||||||
|
NAME=Oreo
|
||||||
|
KIND=Biscuit
|
||||||
|
`)
|
||||||
|
|
||||||
|
func TestWriteConfigDotEnv(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
v := New()
|
||||||
|
v.SetFs(fs)
|
||||||
|
v.SetConfigName("c")
|
||||||
|
v.SetConfigType("env")
|
||||||
|
err := v.ReadConfig(bytes.NewBuffer(dotenvWriteExpected))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := v.WriteConfigAs("c.env"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The TOML String method does not order the contents.
|
||||||
|
// Therefore, we must read the generated file and compare the data.
|
||||||
|
v2 := New()
|
||||||
|
v2.SetFs(fs)
|
||||||
|
v2.SetConfigName("c")
|
||||||
|
v2.SetConfigType("env")
|
||||||
|
v2.SetConfigFile("c.env")
|
||||||
|
err = v2.ReadInConfig()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, v.GetString("title"), v2.GetString("title"))
|
||||||
|
assert.Equal(t, v.GetString("type"), v2.GetString("type"))
|
||||||
|
assert.Equal(t, v.GetString("kind"), v2.GetString("kind"))
|
||||||
|
}
|
||||||
|
|
||||||
var yamlWriteExpected = []byte(`age: 35
|
var yamlWriteExpected = []byte(`age: 35
|
||||||
beard: true
|
beard: true
|
||||||
clothing:
|
clothing:
|
||||||
|
|
Loading…
Reference in New Issue