From 3620d3d9e17742adf7d8e5503abbe515533a9786 Mon Sep 17 00:00:00 2001 From: mexisme Date: Wed, 12 Jun 2019 08:47:43 +1200 Subject: [PATCH] Support `.env` format files (#528) * Support `.env` format files * Missing "dotenv" from SupportedExtns --- viper.go | 26 +++++++++++++++++-- viper_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/viper.go b/viper.go index a3d37f8..a7fa988 100644 --- a/viper.go +++ b/viper.go @@ -45,6 +45,7 @@ import ( "github.com/spf13/cast" jww "github.com/spf13/jwalterweatherman" "github.com/spf13/pflag" + "github.com/subosito/gotenv" ) // ConfigMarshalError happens when failing to marshal the configuration. @@ -230,7 +231,7 @@ func New() *Viper { // can use it in their testing as well. func Reset() { 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"} } @@ -269,7 +270,7 @@ type RemoteProvider interface { } // 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. var SupportedRemoteProviders = []string{"etcd", "consul"} @@ -1400,6 +1401,15 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error { 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": v.properties = properties.NewProperties() var err error @@ -1465,6 +1475,18 @@ func (v *Viper) marshalWriter(f afero.File, configType string) error { 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": t, err := toml.TreeFromMap(c) if err != nil { diff --git a/viper_test.go b/viper_test.go index a0fc195..60d912a 100644 --- a/viper_test.go +++ b/viper_test.go @@ -64,6 +64,11 @@ organization = "MongoDB" Bio = "MongoDB Chief Developer Advocate & Hacker at Large" 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(`{ "id": "0001", "type": "donut", @@ -136,6 +141,10 @@ func initConfigs() { r = bytes.NewReader(tomlExample) unmarshalReader(r, v.config) + SetConfigType("env") + r = bytes.NewReader(dotenvExample) + unmarshalReader(r, v.config) + SetConfigType("json") remote := bytes.NewReader(remoteExample) unmarshalReader(remote, v.kvstore) @@ -179,6 +188,14 @@ func initTOML() { unmarshalReader(r, v.config) } +func initDotEnv() { + Reset() + SetConfigType("env") + r := bytes.NewReader(dotenvExample) + + unmarshalReader(r, v.config) +} + func initHcl() { Reset() SetConfigType("hcl") @@ -342,6 +359,11 @@ func TestTOML(t *testing.T) { 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) { initHcl() assert.Equal(t, "0001", Get("id")) @@ -470,9 +492,11 @@ func TestSetEnvKeyReplacer(t *testing.T) { func TestAllKeys(t *testing.T) { 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") - 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 allkeys = AllKeys() @@ -756,8 +780,11 @@ func TestFindsNestedKeys(t *testing.T) { "hobbies": []interface{}{ "skateboarding", "snowboarding", "go", }, - "title": "TOML Example", - "newkey": "remote", + "TITLE_DOTENV": "DotEnv Example", + "TYPE_DOTENV": "donut", + "NAME_DOTENV": "Cake", + "title": "TOML Example", + "newkey": "remote", "batters": map[string]interface{}{ "batter": []interface{}{ map[string]interface{}{ @@ -1077,6 +1104,43 @@ func TestWriteConfigTOML(t *testing.T) { 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 beard: true clothing: