Skip to content

Commit f0b1103

Browse files
authored
feat: add EnvAny (#2)
1 parent 51c5e82 commit f0b1103

File tree

5 files changed

+180
-24
lines changed

5 files changed

+180
-24
lines changed

any.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package goenvconf
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
)
9+
10+
// EnvAny represents either arbitrary value or an environment reference.
11+
type EnvAny struct {
12+
Value any `json:"value,omitempty" jsonschema:"anyof_required=value" mapstructure:"value" yaml:"value,omitempty"`
13+
Variable *string `json:"env,omitempty" jsonschema:"anyof_required=env" mapstructure:"env" yaml:"env,omitempty"`
14+
}
15+
16+
// NewEnvAny creates an EnvAny instance.
17+
func NewEnvAny(env string, value any) EnvAny {
18+
return EnvAny{
19+
Variable: &env,
20+
Value: value,
21+
}
22+
}
23+
24+
// NewEnvAnyValue creates an EnvAny with a literal value.
25+
func NewEnvAnyValue(value any) EnvAny {
26+
return EnvAny{
27+
Value: value,
28+
}
29+
}
30+
31+
// NewEnvAnyVariable creates an EnvAny with a variable name.
32+
func NewEnvAnyVariable(name string) EnvAny {
33+
return EnvAny{
34+
Variable: &name,
35+
}
36+
}
37+
38+
// UnmarshalJSON implements json.Unmarshaler.
39+
func (ev *EnvAny) UnmarshalJSON(b []byte) error {
40+
type Plain EnvAny
41+
42+
var rawValue Plain
43+
44+
err := json.Unmarshal(b, &rawValue)
45+
if err != nil {
46+
return err
47+
}
48+
49+
if rawValue.Variable != nil && *rawValue.Variable == "" {
50+
return fmt.Errorf("EnvAny: %w", ErrEnvironmentVariableRequired)
51+
}
52+
53+
*ev = EnvAny(rawValue)
54+
55+
return nil
56+
}
57+
58+
// Get gets literal value or from system environment.
59+
func (ev EnvAny) Get() (any, error) {
60+
if ev.Variable != nil {
61+
if *ev.Variable == "" {
62+
return nil, fmt.Errorf("EnvAny: %w", ErrEnvironmentVariableRequired)
63+
}
64+
65+
rawValue := os.Getenv(*ev.Variable)
66+
if rawValue != "" {
67+
var result any
68+
69+
err := json.Unmarshal([]byte(rawValue), &result)
70+
71+
return result, err
72+
}
73+
}
74+
75+
return ev.Value, nil
76+
}
77+
78+
// GetOrDefault returns the default value if the environment value is empty.
79+
func (ev EnvAny) GetOrDefault(defaultValue any) (any, error) {
80+
result, err := ev.Get()
81+
if err != nil {
82+
if errors.Is(err, ErrEnvironmentVariableValueRequired) {
83+
return defaultValue, nil
84+
}
85+
86+
return false, err
87+
}
88+
89+
return result, nil
90+
}

any_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package goenvconf
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"testing"
7+
)
8+
9+
func TestEnvAny(t *testing.T) {
10+
t.Setenv("SOME_FOO", "2.2")
11+
12+
testCases := []struct {
13+
Input EnvAny
14+
Expected any
15+
ErrorMsg string
16+
}{
17+
{
18+
Input: NewEnvAnyValue(map[string]float64{
19+
"foo": 1.1,
20+
}),
21+
Expected: map[string]float64{
22+
"foo": 1.1,
23+
},
24+
},
25+
{
26+
Input: NewEnvAnyVariable("SOME_FOO"),
27+
Expected: float64(2.2),
28+
},
29+
{
30+
Input: EnvAny{},
31+
Expected: nil,
32+
},
33+
{
34+
Input: NewEnvAny("SOME_FOO_2", "baz"),
35+
Expected: "baz",
36+
},
37+
{
38+
Input: EnvAny{
39+
Variable: toPtr(""),
40+
},
41+
ErrorMsg: ErrEnvironmentVariableRequired.Error(),
42+
},
43+
}
44+
45+
for i, tc := range testCases {
46+
t.Run(fmt.Sprint(i), func(t *testing.T) {
47+
result, err := tc.Input.Get()
48+
if tc.ErrorMsg != "" {
49+
assertErrorContains(t, err, tc.ErrorMsg)
50+
} else {
51+
assertNilError(t, err)
52+
assertDeepEqual(t, result, tc.Expected)
53+
}
54+
})
55+
}
56+
57+
t.Run("json_decode", func(t *testing.T) {
58+
var ev EnvAny
59+
assertNilError(t, json.Unmarshal([]byte(`{"env": "SOME_FOO"}`), &ev))
60+
result, err := ev.GetOrDefault(0)
61+
assertNilError(t, err)
62+
assertDeepEqual(t, float64(2.2), result)
63+
})
64+
}

environment.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
)
1111

1212
var (
13-
errEnvironmentValueRequired = errors.New("require either value or env")
14-
errEnvironmentVariableRequired = errors.New("the environment variable name is empty")
15-
errEnvironmentVariableValueRequired = errors.New("the environment variable value is empty")
13+
errEnvironmentValueRequired = errors.New("require either value or env")
14+
// ErrEnvironmentVariableRequired the error happens when the name of environment variable is empty.
15+
ErrEnvironmentVariableRequired = errors.New("the environment variable name is empty")
16+
// ErrEnvironmentVariableValueRequired the error happens when the value from environment variable is empty.
17+
ErrEnvironmentVariableValueRequired = errors.New("the environment variable value is empty")
1618
)
1719

1820
// EnvString represents either a literal string or an environment reference.
@@ -96,7 +98,7 @@ func (ev EnvString) Get() (string, error) {
9698
func (ev EnvString) GetOrDefault(defaultValue string) (string, error) {
9799
result, err := ev.Get()
98100
if err != nil {
99-
if errors.Is(err, errEnvironmentVariableValueRequired) {
101+
if errors.Is(err, ErrEnvironmentVariableValueRequired) {
100102
return defaultValue, nil
101103
}
102104

@@ -182,7 +184,7 @@ func (ev EnvInt) Get() (int64, error) {
182184
func (ev EnvInt) GetOrDefault(defaultValue int64) (int64, error) {
183185
result, err := ev.Get()
184186
if err != nil {
185-
if errors.Is(err, errEnvironmentVariableValueRequired) {
187+
if errors.Is(err, ErrEnvironmentVariableValueRequired) {
186188
return defaultValue, nil
187189
}
188190

@@ -266,7 +268,7 @@ func (ev EnvBool) Get() (bool, error) {
266268
func (ev EnvBool) GetOrDefault(defaultValue bool) (bool, error) {
267269
result, err := ev.Get()
268270
if err != nil {
269-
if errors.Is(err, errEnvironmentVariableValueRequired) {
271+
if errors.Is(err, ErrEnvironmentVariableValueRequired) {
270272
return defaultValue, nil
271273
}
272274

@@ -350,7 +352,7 @@ func (ev EnvFloat) Get() (float64, error) {
350352
func (ev EnvFloat) GetOrDefault(defaultValue float64) (float64, error) {
351353
result, err := ev.Get()
352354
if err != nil {
353-
if errors.Is(err, errEnvironmentVariableValueRequired) {
355+
if errors.Is(err, ErrEnvironmentVariableValueRequired) {
354356
return defaultValue, nil
355357
}
356358

@@ -626,8 +628,8 @@ func (ev EnvMapBool) Get() (map[string]bool, error) {
626628

627629
func getEnvVariableValueRequiredError(envName *string) error {
628630
if envName != nil {
629-
return fmt.Errorf("%s: %w", *envName, errEnvironmentVariableValueRequired)
631+
return fmt.Errorf("%s: %w", *envName, ErrEnvironmentVariableValueRequired)
630632
}
631633

632-
return errEnvironmentVariableValueRequired
634+
return ErrEnvironmentVariableValueRequired
633635
}

environment_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestEnvString(t *testing.T) {
3535
Input: EnvString{
3636
Variable: toPtr(""),
3737
},
38-
ErrorMsg: errEnvironmentVariableRequired.Error(),
38+
ErrorMsg: ErrEnvironmentVariableRequired.Error(),
3939
},
4040
}
4141

@@ -97,7 +97,7 @@ func TestEnvBool(t *testing.T) {
9797
Input: EnvBool{
9898
Variable: toPtr(""),
9999
},
100-
ErrorMsg: errEnvironmentVariableRequired.Error(),
100+
ErrorMsg: ErrEnvironmentVariableRequired.Error(),
101101
},
102102
}
103103

@@ -106,7 +106,7 @@ func TestEnvBool(t *testing.T) {
106106
result, err := tc.Input.Get()
107107
if tc.ErrorMsg != "" {
108108
assertErrorContains(t, err, tc.ErrorMsg)
109-
if tc.ErrorMsg == errEnvironmentVariableValueRequired.Error() {
109+
if tc.ErrorMsg == ErrEnvironmentVariableValueRequired.Error() {
110110
newValue, err := tc.Input.GetOrDefault(true)
111111
assertNilError(t, err)
112112
assertDeepEqual(t, newValue, true)
@@ -154,7 +154,7 @@ func TestEnvInt(t *testing.T) {
154154
},
155155
{
156156
Input: NewEnvIntVariable("SOME_FOO_2"),
157-
ErrorMsg: errEnvironmentVariableValueRequired.Error(),
157+
ErrorMsg: ErrEnvironmentVariableValueRequired.Error(),
158158
},
159159
{
160160
Input: EnvInt{},
@@ -168,7 +168,7 @@ func TestEnvInt(t *testing.T) {
168168
Input: EnvInt{
169169
Variable: toPtr(""),
170170
},
171-
ErrorMsg: errEnvironmentVariableRequired.Error(),
171+
ErrorMsg: ErrEnvironmentVariableRequired.Error(),
172172
},
173173
}
174174

@@ -177,7 +177,7 @@ func TestEnvInt(t *testing.T) {
177177
result, err := tc.Input.Get()
178178
if tc.ErrorMsg != "" {
179179
assertErrorContains(t, err, tc.ErrorMsg)
180-
if tc.ErrorMsg == errEnvironmentVariableValueRequired.Error() {
180+
if tc.ErrorMsg == ErrEnvironmentVariableValueRequired.Error() {
181181
newValue, err := tc.Input.GetOrDefault(100)
182182
assertNilError(t, err)
183183
assertDeepEqual(t, newValue, int64(100))
@@ -219,7 +219,7 @@ func TestEnvFloat(t *testing.T) {
219219
},
220220
{
221221
Input: NewEnvFloatVariable("SOME_FOO_2"),
222-
ErrorMsg: errEnvironmentVariableValueRequired.Error(),
222+
ErrorMsg: ErrEnvironmentVariableValueRequired.Error(),
223223
},
224224
{
225225
Input: EnvFloat{},
@@ -233,7 +233,7 @@ func TestEnvFloat(t *testing.T) {
233233
Input: EnvFloat{
234234
Variable: toPtr(""),
235235
},
236-
ErrorMsg: errEnvironmentVariableRequired.Error(),
236+
ErrorMsg: ErrEnvironmentVariableRequired.Error(),
237237
},
238238
}
239239

@@ -242,7 +242,7 @@ func TestEnvFloat(t *testing.T) {
242242
result, err := tc.Input.Get()
243243
if tc.ErrorMsg != "" {
244244
assertErrorContains(t, err, tc.ErrorMsg)
245-
if tc.ErrorMsg == errEnvironmentVariableValueRequired.Error() {
245+
if tc.ErrorMsg == ErrEnvironmentVariableValueRequired.Error() {
246246
newValue, err := tc.Input.GetOrDefault(100.5)
247247
assertNilError(t, err)
248248
assertDeepEqual(t, newValue, float64(100.5))
@@ -301,7 +301,7 @@ func TestEnvMapBool(t *testing.T) {
301301
Input: EnvMapBool{
302302
Variable: toPtr(""),
303303
},
304-
ErrorMsg: errEnvironmentVariableRequired.Error(),
304+
ErrorMsg: ErrEnvironmentVariableRequired.Error(),
305305
},
306306
}
307307

@@ -363,7 +363,7 @@ func TestEnvMapInt(t *testing.T) {
363363
Input: EnvMapInt{
364364
Variable: toPtr(""),
365365
},
366-
ErrorMsg: errEnvironmentVariableRequired.Error(),
366+
ErrorMsg: ErrEnvironmentVariableRequired.Error(),
367367
},
368368
}
369369

@@ -425,7 +425,7 @@ func TestEnvMapFloat(t *testing.T) {
425425
Input: EnvMapFloat{
426426
Variable: toPtr(""),
427427
},
428-
ErrorMsg: errEnvironmentVariableRequired.Error(),
428+
ErrorMsg: ErrEnvironmentVariableRequired.Error(),
429429
},
430430
}
431431

@@ -488,7 +488,7 @@ func TestEnvMapString(t *testing.T) {
488488
Input: EnvMapString{
489489
Variable: toPtr(""),
490490
},
491-
ErrorMsg: errEnvironmentVariableRequired.Error(),
491+
ErrorMsg: ErrEnvironmentVariableRequired.Error(),
492492
},
493493
}
494494

utils.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,15 @@ func validateEnvironmentValue[T any](value *T, variable *string) error {
141141
}
142142

143143
if variable != nil && *variable == "" {
144-
return errEnvironmentVariableRequired
144+
return ErrEnvironmentVariableRequired
145145
}
146146

147147
return nil
148148
}
149149

150150
func validateEnvironmentMapValue(variable *string) error {
151151
if variable != nil && *variable == "" {
152-
return errEnvironmentVariableRequired
152+
return ErrEnvironmentVariableRequired
153153
}
154154

155155
return nil

0 commit comments

Comments
 (0)