Skip to content

Commit 362b2e9

Browse files
authored
feat: implement NameSep option (#62)
1 parent bc61971 commit 362b2e9

File tree

5 files changed

+73
-11
lines changed

5 files changed

+73
-11
lines changed

README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,14 @@ In this case, the environment variables declared by its fields are prefixed with
9393
This rule is applied recursively to all nested structs.
9494

9595
```go
96-
os.Setenv("DB_HOST", "localhost")
97-
os.Setenv("DB_PORT", "5432")
96+
os.Setenv("DBHOST", "localhost")
97+
os.Setenv("DBPORT", "5432")
9898

9999
var cfg struct {
100100
DB struct {
101101
Host string `env:"HOST"`
102102
Port int `env:"PORT"`
103-
} `env:"DB_"`
103+
} `env:"DB"`
104104
}
105105
if err := env.Load(&cfg, nil); err != nil {
106106
fmt.Println(err)
@@ -182,6 +182,29 @@ if err := env.Load(&cfg, &env.Options{SliceSep: ","}); err != nil {
182182
fmt.Println(cfg.Ports) // [8080 8081 8082]
183183
```
184184

185+
### Name separator
186+
187+
By default, environment variable names are concatenated from nested struct tags as is.
188+
If `Options.NameSep` is not empty, it is used as the separator:
189+
190+
```go
191+
os.Setenv("DB_HOST", "localhost")
192+
os.Setenv("DB_PORT", "5432")
193+
194+
var cfg struct {
195+
DB struct {
196+
Host string `env:"HOST"`
197+
Port int `env:"PORT"`
198+
} `env:"DB"`
199+
}
200+
if err := env.Load(&cfg, &env.Options{NameSep: "_"}); err != nil {
201+
fmt.Println(err)
202+
}
203+
204+
fmt.Println(cfg.DB.Host) // localhost
205+
fmt.Println(cfg.DB.Port) // 5432
206+
```
207+
185208
### Source
186209

187210
By default, `Load` retrieves environment variables directly from OS.

env.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
type Options struct {
1313
Source Source // The source of environment variables. The default is [OS].
1414
SliceSep string // The separator used to parse slice values. The default is space.
15+
NameSep string // The separator used to concatenate environment variable names from nested struct tags. The default is an empty string.
1516
}
1617

1718
// NotSetError is returned when environment variables are marked as required but not set.
@@ -76,7 +77,7 @@ func Load(cfg any, opts *Options) error {
7677
}
7778

7879
v := pv.Elem()
79-
vars := parseVars(v)
80+
vars := parseVars(v, opts)
8081
cache[v.Type()] = vars
8182

8283
var notset []string
@@ -111,7 +112,7 @@ func Load(cfg any, opts *Options) error {
111112
return nil
112113
}
113114

114-
func parseVars(v reflect.Value) []Var {
115+
func parseVars(v reflect.Value, opts *Options) []Var {
115116
var vars []Var
116117

117118
for i := 0; i < v.NumField(); i++ {
@@ -126,9 +127,9 @@ func parseVars(v reflect.Value) []Var {
126127
sf := v.Type().Field(i)
127128
value, ok := sf.Tag.Lookup("env")
128129
if ok {
129-
prefix = value
130+
prefix = value + opts.NameSep
130131
}
131-
for _, v := range parseVars(field) {
132+
for _, v := range parseVars(field, opts) {
132133
v.Name = prefix + v.Name
133134
vars = append(vars, v)
134135
}

env_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ func TestLoad(t *testing.T) {
6161
assert.Panics[E](t, load, "env: `required` and `default` can't be used simultaneously")
6262
})
6363

64+
t.Run("nested struct w/ and w/o tag", func(t *testing.T) {
65+
m := env.Map{"A_FOO": "1", "BAR": "2"}
66+
67+
var cfg struct {
68+
A struct {
69+
Foo int `env:"FOO"`
70+
} `env:"A"`
71+
B struct {
72+
Bar int `env:"BAR"`
73+
}
74+
}
75+
err := env.Load(&cfg, &env.Options{Source: m, NameSep: "_"})
76+
assert.NoErr[F](t, err)
77+
assert.Equal[E](t, cfg.A.Foo, 1)
78+
assert.Equal[E](t, cfg.B.Bar, 2)
79+
})
80+
6481
t.Run("unsupported type", func(t *testing.T) {
6582
m := env.Map{"FOO": "1+2i"}
6683

example_test.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,38 @@ func ExampleLoad_nestedStruct() {
5252
// Output: 8080
5353
}
5454

55-
func ExampleLoad_nestedStructPrefixed() {
55+
func ExampleLoad_nestedStructWithPrefix() {
56+
os.Setenv("DBHOST", "localhost")
57+
os.Setenv("DBPORT", "5432")
58+
59+
var cfg struct {
60+
DB struct {
61+
Host string `env:"HOST"`
62+
Port int `env:"PORT"`
63+
} `env:"DB"`
64+
}
65+
if err := env.Load(&cfg, nil); err != nil {
66+
fmt.Println(err)
67+
}
68+
69+
fmt.Println(cfg.DB.Host)
70+
fmt.Println(cfg.DB.Port)
71+
// Output:
72+
// localhost
73+
// 5432
74+
}
75+
76+
func ExampleLoad_nestedStructWithPrefixAndSeparator() {
5677
os.Setenv("DB_HOST", "localhost")
5778
os.Setenv("DB_PORT", "5432")
5879

5980
var cfg struct {
6081
DB struct {
6182
Host string `env:"HOST"`
6283
Port int `env:"PORT"`
63-
} `env:"DB_"`
84+
} `env:"DB"`
6485
}
65-
if err := env.Load(&cfg, nil); err != nil {
86+
if err := env.Load(&cfg, &env.Options{NameSep: "_"}); err != nil {
6687
fmt.Println(err)
6788
}
6889

usage.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func Usage(cfg any, w io.Writer, opts *Options) {
4545
v := pv.Elem()
4646
vars, ok := cache[v.Type()]
4747
if !ok {
48-
vars = parseVars(v)
48+
vars = parseVars(v, opts)
4949
}
5050

5151
if u, ok := cfg.(interface {

0 commit comments

Comments
 (0)