From 625998fc966e5be56283ca3b032457c4e41d9676 Mon Sep 17 00:00:00 2001 From: Taras Bunyk Date: Wed, 13 Mar 2024 14:56:51 +0100 Subject: [PATCH] feat: support prefixes for nested structs (#53) --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ env.go | 17 ++++++++++++++++- example_test.go | 21 +++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f3c7840..4fb0b75 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,48 @@ fmt.Println(cfg.Port) // 8080 See the `strconv.Parse*` functions for the parsing rules. User-defined types can be used by implementing the `encoding.TextUnmarshaler` interface. +### Nested structs + +Nested struct of any depth level are supported, +allowing grouping of related environment variables. + +```go +os.Setenv("HTTP_PORT", "8080") + +var cfg struct { + HTTP struct { + Port int `env:"HTTP_PORT"` + } +} +if err := env.Load(&cfg, nil); err != nil { + fmt.Println(err) +} + +fmt.Println(cfg.HTTP.Port) // 8080 +``` + +A nested struct can have the optional `env:"PREFIX"` tag. +In this case, the environment variables declared by its fields are prefixed with PREFIX. +This rule is applied recursively to all nested structs. + +```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, nil); err != nil { + fmt.Println(err) +} + +fmt.Println(cfg.DB.Host) // localhost +fmt.Println(cfg.DB.Port) // 5432 +``` + ### Default values Default values can be specified using the `default:"VALUE"` struct tag: diff --git a/env.go b/env.go index b539302..e08a741 100644 --- a/env.go +++ b/env.go @@ -48,6 +48,12 @@ func (e *NotSetError) Error() string { // See the [strconv].Parse* functions for the parsing rules. // User-defined types can be used by implementing the [encoding.TextUnmarshaler] interface. // +// Nested struct of any depth level are supported, +// allowing grouping of related environment variables. +// A nested struct can have the optional `env:"PREFIX"` tag. +// In this case, the environment variables declared by its fields are prefixed with PREFIX. +// This rule is applied recursively to all nested structs. +// // Default values can be specified using the `default:"VALUE"` struct tag. // // The name of an environment variable can be followed by comma-separated options: @@ -116,7 +122,16 @@ func parseVars(v reflect.Value) []Var { // special case: a nested struct, parse its fields recursively. if kindOf(field, reflect.Struct) && !implements(field, unmarshalerIface) { - vars = append(vars, parseVars(field)...) + var prefix string + sf := v.Type().Field(i) + value, ok := sf.Tag.Lookup("env") + if ok { + prefix = value + } + for _, v := range parseVars(field) { + v.Name = prefix + v.Name + vars = append(vars, v) + } continue } diff --git a/example_test.go b/example_test.go index 218b7cc..442652a 100644 --- a/example_test.go +++ b/example_test.go @@ -52,6 +52,27 @@ func ExampleLoad_nestedStruct() { // Output: 8080 } +func ExampleLoad_nestedStructPrefixed() { + 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, nil); err != nil { + fmt.Println(err) + } + + fmt.Println(cfg.DB.Host) + fmt.Println(cfg.DB.Port) + // Output: + // localhost + // 5432 +} + func ExampleLoad_required() { os.Unsetenv("PORT")