Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,14 @@ In this case, the environment variables declared by its fields are prefixed with
This rule is applied recursively to all nested structs.

```go
os.Setenv("DB_HOST", "localhost")
os.Setenv("DB_PORT", "5432")
os.Setenv("DBHOST", "localhost")
os.Setenv("DBPORT", "5432")

var cfg struct {
DB struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
} `env:"DB_"`
} `env:"DB"`
}
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
Expand Down Expand Up @@ -182,6 +182,29 @@ if err := env.Load(&cfg, &env.Options{SliceSep: ","}); err != nil {
fmt.Println(cfg.Ports) // [8080 8081 8082]
```

### Name separator

By default, environment variable names are concatenated from nested struct tags as is.
If `Options.NameSep` is not empty, it is used as the separator:

```go
os.Setenv("DB_HOST", "localhost")
os.Setenv("DB_PORT", "5432")

var cfg struct {
DB struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
} `env:"DB"`
}
if err := env.Load(&cfg, &env.Options{NameSep: "_"}); err != nil {
fmt.Println(err)
}

fmt.Println(cfg.DB.Host) // localhost
fmt.Println(cfg.DB.Port) // 5432
```

### Source

By default, `Load` retrieves environment variables directly from OS.
Expand Down
9 changes: 5 additions & 4 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
type Options struct {
Source Source // The source of environment variables. The default is [OS].
SliceSep string // The separator used to parse slice values. The default is space.
NameSep string // The separator used to concatenate environment variable names from nested struct tags. The default is an empty string.
}

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

v := pv.Elem()
vars := parseVars(v)
vars := parseVars(v, opts)
cache[v.Type()] = vars

var notset []string
Expand Down Expand Up @@ -111,7 +112,7 @@ func Load(cfg any, opts *Options) error {
return nil
}

func parseVars(v reflect.Value) []Var {
func parseVars(v reflect.Value, opts *Options) []Var {
var vars []Var

for i := 0; i < v.NumField(); i++ {
Expand All @@ -126,9 +127,9 @@ func parseVars(v reflect.Value) []Var {
sf := v.Type().Field(i)
value, ok := sf.Tag.Lookup("env")
if ok {
prefix = value
prefix = value + opts.NameSep
}
for _, v := range parseVars(field) {
for _, v := range parseVars(field, opts) {
v.Name = prefix + v.Name
vars = append(vars, v)
}
Expand Down
17 changes: 17 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ func TestLoad(t *testing.T) {
assert.Panics[E](t, load, "env: `required` and `default` can't be used simultaneously")
})

t.Run("nested struct w/ and w/o tag", func(t *testing.T) {
m := env.Map{"A_FOO": "1", "BAR": "2"}

var cfg struct {
A struct {
Foo int `env:"FOO"`
} `env:"A"`
B struct {
Bar int `env:"BAR"`
}
}
err := env.Load(&cfg, &env.Options{Source: m, NameSep: "_"})
assert.NoErr[F](t, err)
assert.Equal[E](t, cfg.A.Foo, 1)
assert.Equal[E](t, cfg.B.Bar, 2)
})

t.Run("unsupported type", func(t *testing.T) {
m := env.Map{"FOO": "1+2i"}

Expand Down
27 changes: 24 additions & 3 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,38 @@ func ExampleLoad_nestedStruct() {
// Output: 8080
}

func ExampleLoad_nestedStructPrefixed() {
func ExampleLoad_nestedStructWithPrefix() {
os.Setenv("DBHOST", "localhost")
os.Setenv("DBPORT", "5432")

var cfg struct {
DB struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
} `env:"DB"`
}
if err := env.Load(&cfg, nil); err != nil {
fmt.Println(err)
}

fmt.Println(cfg.DB.Host)
fmt.Println(cfg.DB.Port)
// Output:
// localhost
// 5432
}

func ExampleLoad_nestedStructWithPrefixAndSeparator() {
os.Setenv("DB_HOST", "localhost")
os.Setenv("DB_PORT", "5432")

var cfg struct {
DB struct {
Host string `env:"HOST"`
Port int `env:"PORT"`
} `env:"DB_"`
} `env:"DB"`
}
if err := env.Load(&cfg, nil); err != nil {
if err := env.Load(&cfg, &env.Options{NameSep: "_"}); err != nil {
fmt.Println(err)
}

Expand Down
2 changes: 1 addition & 1 deletion usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func Usage(cfg any, w io.Writer, opts *Options) {
v := pv.Elem()
vars, ok := cache[v.Type()]
if !ok {
vars = parseVars(v)
vars = parseVars(v, opts)
}

if u, ok := cfg.(interface {
Expand Down