diff --git a/providers/env/env.go b/providers/env/env.go index 5ec252bf..602c43bb 100644 --- a/providers/env/env.go +++ b/providers/env/env.go @@ -12,9 +12,10 @@ import ( // Env implements an environment variables provider. type Env struct { - prefix string - delim string - cb func(key string, value string) (string, interface{}) + prefix string + delim string + environFunc func() []string + cb func(key string, value string) (string, interface{}) } // Provider returns an environment variables provider that returns @@ -31,16 +32,13 @@ type Env struct { // If the callback returns an empty string, the variable will be // ignored. func Provider(prefix, delim string, cb func(s string) string) *Env { - e := &Env{ - prefix: prefix, - delim: delim, - } - if cb != nil { - e.cb = func(key string, value string) (string, interface{}) { + return ProviderWithOptions( + WithPrefix(prefix), + WithDelimiter(delim), + WithCallback(func(key string, value string) (string, interface{}) { return cb(key), value - } - } - return e + }), + ) } // ProviderWithValue works exactly the same as Provider except the callback @@ -48,11 +46,29 @@ func Provider(prefix, delim string, cb func(s string) string) *Env { // to modify both. This is useful for cases where you may want to return // other types like a string slice instead of just a string. func ProviderWithValue(prefix, delim string, cb func(key string, value string) (string, interface{})) *Env { - return &Env{ - prefix: prefix, - delim: delim, - cb: cb, + return ProviderWithOptions( + WithPrefix(prefix), + WithDelimiter(delim), + WithCallback(cb), + ) +} + +// ProviderWithOptions returns an environment variables provider that can be fine-tuned +// by using options. +func ProviderWithOptions(options ...Option) *Env { + e := &Env{ + prefix: "", + delim: ".", + cb: nil, + environFunc: func() []string { + return os.Environ() + }, } + + for _, option := range options { + option(e) + } + return e } // ReadBytes is not supported by the env provider. @@ -65,7 +81,7 @@ func (e *Env) ReadBytes() ([]byte, error) { func (e *Env) Read() (map[string]interface{}, error) { // Collect the environment variable keys. var keys []string - for _, k := range os.Environ() { + for _, k := range e.environFunc() { if e.prefix != "" { if strings.HasPrefix(k, e.prefix) { keys = append(keys, k) diff --git a/providers/env/env_test.go b/providers/env/env_test.go index 2717c711..ee4f2f77 100644 --- a/providers/env/env_test.go +++ b/providers/env/env_test.go @@ -1,14 +1,14 @@ package env import ( - "github.com/stretchr/testify/assert" "os" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestProvider(t *testing.T) { - testCases := []struct { name string prefix string @@ -18,17 +18,7 @@ func TestProvider(t *testing.T) { expKey string expValue string cb func(key string) string - want *Env }{ - { - name: "Nil cb", - prefix: "TESTVAR_", - delim: ".", - want: &Env{ - prefix: "TESTVAR_", - delim: ".", - }, - }, { name: "Simple cb", prefix: "TESTVAR_", @@ -40,19 +30,6 @@ func TestProvider(t *testing.T) { cb: func(key string) string { return strings.ToLower(key) }, - want: &Env{ - prefix: "TESTVAR_", - delim: ".", - }, - }, - { - name: "Empty string nil cb", - prefix: "", - delim: ".", - want: &Env{ - prefix: "", - delim: ".", - }, }, { name: "Cb is given", @@ -71,93 +48,187 @@ func TestProvider(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { gotProvider := Provider(tc.prefix, tc.delim, tc.cb) - if tc.cb == nil { - assert.Equal(t, tc.want, gotProvider) - } - if tc.cb != nil { - k, v := gotProvider.cb(tc.key, tc.value) - assert.Equal(t, tc.expKey, k) - assert.Equal(t, tc.expValue, v) - } + k, v := gotProvider.cb(tc.key, tc.value) + assert.Equal(t, tc.expKey, k) + assert.Equal(t, tc.expValue, v) }) } } func TestProviderWithValue(t *testing.T) { testCases := []struct { - name string - prefix string - delim string - cb func(key string, value string) (string, interface{}) - nilCallback bool - want *Env + name string + prefix string + delim string + cb func(key string, value string) (string, interface{}) + want *Env + }{ + { + name: "Custom cb function", + prefix: "TEST_", + delim: ".", + cb: func(key string, value string) (string, interface{}) { + key = strings.Replace(strings.TrimPrefix(strings.ToLower(key), "test_"), "_", ".", -1) + return key, value + }, + want: &Env{ + prefix: "TEST_", + delim: ".", + environFunc: func() []string { + return os.Environ() + }, + cb: func(key string, value string) (string, interface{}) { + key = strings.Replace(strings.TrimPrefix(strings.ToLower(key), "test_"), "_", ".", -1) + return key, value + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := ProviderWithValue(tc.prefix, tc.delim, tc.cb) + keyGot, valGot := got.cb("test_key_env_1", "test_val") + keyWant, valWant := tc.want.cb("test_key_env_1", "test_val") + assert.Equal(t, tc.prefix, got.prefix) + assert.Equal(t, tc.delim, got.delim) + assert.Equal(t, keyWant, keyGot) + assert.Equal(t, valWant, valGot) + }) + } +} + +func TestProviderWithOptions(t *testing.T) { + testCases := []struct { + name string + options []Option + want *Env }{ { - name: "Nil cb", - prefix: "TEST_", - delim: ".", - nilCallback: true, + name: "Nil cb", + options: []Option{ + WithPrefix("TEST_"), + WithDelimiter("."), + WithEnviron([]string{"FOO=BAR"}), + WithCallback(nil), + }, want: &Env{ prefix: "TEST_", delim: ".", + environFunc: func() []string { + return []string{"FOO=BAR"} + }, }, }, { - name: "Empty string nil cb", - prefix: "", - delim: ".", - nilCallback: true, + name: "Empty prefix nil cb", + options: []Option{ + WithPrefix(""), + WithDelimiter("."), + WithEnviron([]string{"FOO=BAR"}), + WithCallback(nil), + }, want: &Env{ prefix: "", delim: ".", + environFunc: func() []string { + return []string{"FOO=BAR"} + }, }, }, { - name: "Return the same key-value pair in cb", - prefix: "TEST_", - delim: ".", - cb: func(key string, value string) (string, interface{}) { - return key, value + name: "Return the same key-value pair in cb", + options: []Option{ + WithPrefix("TEST_"), + WithDelimiter("."), + WithEnviron([]string{"FOO=BAR"}), + WithCallback(func(key string, value string) (string, interface{}) { + return key, value + }), }, want: &Env{ prefix: "TEST_", delim: ".", + environFunc: func() []string { + return []string{"FOO=BAR"} + }, cb: func(key string, value string) (string, interface{}) { return key, value }, }, }, { - name: "Custom cb function", - prefix: "TEST_", - delim: ".", - cb: func(key string, value string) (string, interface{}) { - key = strings.Replace(strings.TrimPrefix(strings.ToLower(key), "test_"), "_", ".", -1) - return key, value + name: "Custom cb function", + options: []Option{ + WithPrefix("TEST_"), + WithDelimiter("."), + WithEnviron([]string{"FOO=BAR"}), + WithCallback(func(key string, value string) (string, interface{}) { + key = strings.Replace(strings.TrimPrefix(strings.ToLower(key), "test_"), "_", ".", -1) + return key, value + }), }, want: &Env{ prefix: "TEST_", delim: ".", + environFunc: func() []string { + return []string{"FOO=BAR"} + }, cb: func(key string, value string) (string, interface{}) { key = strings.Replace(strings.TrimPrefix(strings.ToLower(key), "test_"), "_", ".", -1) return key, value }, }, }, + { + name: "with custom environment slice", + options: []Option{ + WithPrefix("TEST_"), + WithDelimiter("."), + WithEnviron([]string{"FOO=BAR"}), + }, + want: &Env{ + prefix: "TEST_", + delim: ".", + environFunc: func() []string { + return []string{"FOO=BAR"} + }, + }, + }, + { + name: "with custom environment map", + options: []Option{ + WithPrefix("TEST_"), + WithDelimiter("."), + WithEnvironMap(map[string]string{ + "FOO": "BAR", + }), + }, + want: &Env{ + prefix: "TEST_", + delim: ".", + environFunc: func() []string { + return []string{"FOO=BAR"} + }, + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - got := ProviderWithValue(tc.prefix, tc.delim, tc.cb) - if tc.nilCallback { + got := ProviderWithOptions(tc.options...) + if got.cb == nil && got.environFunc == nil { assert.Equal(t, tc.want, got) } else { - keyGot, valGot := got.cb("test_key_env_1", "test_val") - keyWant, valWant := tc.want.cb("test_key_env_1", "test_val") - assert.Equal(t, tc.prefix, got.prefix) - assert.Equal(t, tc.delim, got.delim) - assert.Equal(t, keyWant, keyGot) - assert.Equal(t, valWant, valGot) + if got.cb != nil { + keyGot, valGot := got.cb("test_key_env_1", "test_val") + keyWant, valWant := tc.want.cb("test_key_env_1", "test_val") + assert.Equal(t, keyWant, keyGot) + assert.Equal(t, valWant, valGot) + } + if got.environFunc != nil { + assert.Equal(t, tc.want.environFunc(), got.environFunc()) + } } }) } @@ -166,30 +237,30 @@ func TestProviderWithValue(t *testing.T) { func TestRead(t *testing.T) { testCases := []struct { name string - key string - value string expKey string expValue string env *Env }{ { name: "No cb", - key: "TEST_KEY", - value: "TEST_VAL", expKey: "TEST_KEY", expValue: "TEST_VAL", env: &Env{ delim: ".", + environFunc: func() []string { + return []string{"TEST_KEY=TEST_VAL"} + }, }, }, { name: "cb given", - key: "TEST_KEY", - value: "TEST_VAL", expKey: "test.key", expValue: "TEST_VAL", env: &Env{ delim: "_", + environFunc: func() []string { + return []string{"TEST_KEY=TEST_VAL"} + }, cb: func(key string, value string) (string, interface{}) { return strings.Replace(strings.ToLower(key), "_", ".", -1), value }, @@ -197,13 +268,14 @@ func TestRead(t *testing.T) { }, { name: "No cb - prefix given", - key: "TEST_KEY", - value: "TEST_VAL", expKey: "test.key", expValue: "TEST_VAL", env: &Env{ prefix: "TEST", delim: "/", + environFunc: func() []string { + return []string{"TEST_KEY=TEST_VAL"} + }, cb: func(key string, value string) (string, interface{}) { return strings.Replace(strings.ToLower(key), "_", ".", -1), value }, @@ -211,22 +283,24 @@ func TestRead(t *testing.T) { }, { name: "Path value", - key: "TEST_DIR", - value: "/test/dir/file", expKey: "TEST_DIR", expValue: "/test/dir/file", env: &Env{ + environFunc: func() []string { + return []string{"TEST_DIR=/test/dir/file"} + }, delim: ".", }, }, { name: "Replace value with underscore", - key: "TEST_DIR", - value: "/test/dir/file", expKey: "TEST_DIR", expValue: "_test_dir_file", env: &Env{ delim: ".", + environFunc: func() []string { + return []string{"TEST_DIR=/test/dir/file"} + }, cb: func(key string, value string) (string, interface{}) { return key, strings.Replace(strings.ToLower(value), "/", "_", -1) }, @@ -234,11 +308,12 @@ func TestRead(t *testing.T) { }, { name: "Empty value", - key: "KEY", - value: "", expKey: "KEY", expValue: "", env: &Env{ + environFunc: func() []string { + return []string{"KEY="} + }, delim: ".", }, }, @@ -246,10 +321,6 @@ func TestRead(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err := os.Setenv(tc.key, tc.value) - assert.Nil(t, err) - defer os.Unsetenv(tc.key) - envs, err := tc.env.Read() assert.Nil(t, err) v, ok := envs[tc.expKey] diff --git a/providers/env/options.go b/providers/env/options.go new file mode 100644 index 00000000..cd9ea56f --- /dev/null +++ b/providers/env/options.go @@ -0,0 +1,54 @@ +package env + +type Option func(*Env) + +// WithPrefix sets the environment prefix. Only the env vars with the prefix are captured. +func WithPrefix(prefix string) Option { + return func(env *Env) { + env.prefix = prefix + } +} + +// WithDelimiter sets the delimiter to split the environment variable into its parts. +// For example the delimiter "." will convert the key `parent.child.key: 1` +// to `{parent: {child: {key: 1}}}`. +func WithDelimiter(delim string) Option { + return func(env *Env) { + env.delim = delim + } +} + +// WithCallback sets the function that will be called for each environment variable. +// This is useful for cases where you may want to modify the variable or value before it gets passed on. +// If the callback returns an empty string, the variable will be +// ignored. +func WithCallback(cb func(key string, value string) (string, interface{})) Option { + return func(env *Env) { + env.cb = cb + } +} + +// WithEnvironFunc sets the environment using a function +func WithEnvironFunc(environFunc func() []string) Option { + return func(env *Env) { + env.environFunc = environFunc + } +} + +// WithEnviron sets the environment using a traditional environment slice. +func WithEnviron(environ []string) Option { + return WithEnvironFunc(func() []string { + return environ + }) +} + +// WithEnvironMap sets the environment using a map. +func WithEnvironMap(environ map[string]string) Option { + return WithEnvironFunc(func() []string { + s := make([]string, 0, len(environ)) + for k, v := range environ { + s = append(s, k+"="+v) + } + return s + }) +}