diff --git a/README.md b/README.md index 8710627..e499e1d 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. diff --git a/env.go b/env.go index e08a741..1712861 100644 --- a/env.go +++ b/env.go @@ -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. @@ -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 @@ -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++ { @@ -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) } diff --git a/env_test.go b/env_test.go index 1cc4f6c..74a5061 100644 --- a/env_test.go +++ b/env_test.go @@ -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"} diff --git a/example_test.go b/example_test.go index d0db336..aa1f4d9 100644 --- a/example_test.go +++ b/example_test.go @@ -52,7 +52,28 @@ 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") @@ -60,9 +81,9 @@ func ExampleLoad_nestedStructPrefixed() { 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) } diff --git a/usage.go b/usage.go index ef4a610..b248557 100644 --- a/usage.go +++ b/usage.go @@ -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 {