Skip to content

Commit

Permalink
feat: implement NameSep option (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
bunyk authored May 2, 2024
1 parent bc61971 commit 362b2e9
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 11 deletions.
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

0 comments on commit 362b2e9

Please sign in to comment.