diff --git a/.golangci.yml b/.golangci.yml index 641471c..ef926a0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,7 +7,6 @@ linters: - govet - ineffassign - staticcheck - - typecheck - unused # disabled by default: - gocritic diff --git a/README.md b/README.md index 0c96b67..10d549e 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,14 @@ which simplifies config management and improves code readability. ## 🚀 Features * Support for all common types and user-defined types -* Options: [required](#required), [expand](#expand), [slice separator](#slice-separator) * Configurable [source](#source) of environment variables +* Options: [required](#required), [expand](#expand), [slice separator](#slice-separator), [name separator](#name-separator) * Auto-generated [usage message](#usage-message) ## 📦 Install Go 1.20+ + ```shell go get go-simpler.org/env ``` @@ -38,7 +39,7 @@ go get go-simpler.org/env It loads environment variables into the given struct. The struct fields must have the `env:"VAR"` struct tag, -where VAR is the name of the corresponding environment variable. +where `VAR` is the name of the corresponding environment variable. Unexported fields are ignored. ```go @@ -74,33 +75,35 @@ Nested struct of any depth level are supported, allowing grouping of related environment variables. ```go -os.Setenv("HTTP_PORT", "8080") +os.Setenv("DB_HOST", "localhost") +os.Setenv("DB_PORT", "5432") var cfg struct { - HTTP struct { - Port int `env:"HTTP_PORT"` + DB struct { + Host string `env:"DB_HOST"` + Port int `env:"DB_PORT"` } } if err := env.Load(&cfg, nil); err != nil { fmt.Println(err) } -fmt.Println(cfg.HTTP.Port) // 8080 +fmt.Println(cfg.DB.Host) // localhost +fmt.Println(cfg.DB.Port) // 5432 ``` -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. +If a nested struct has the optional `env:"PREFIX"` tag, +the environment variables declared by its fields are prefixed with `PREFIX`. ```go -os.Setenv("DBHOST", "localhost") -os.Setenv("DBPORT", "5432") +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 { fmt.Println(err) @@ -112,7 +115,7 @@ fmt.Println(cfg.DB.Port) // 5432 ### Default values -Default values can be specified using the `default:"VALUE"` struct tag: +Default values can be specified using the `default:"VALUE"` struct tag. ```go os.Unsetenv("PORT") @@ -167,7 +170,7 @@ fmt.Println(cfg.Addr) // localhost:8080 ### Slice separator Space is the default separator used to parse slice values. -It can be changed with `Options.SliceSep`: +It can be changed with `Options.SliceSep`. ```go os.Setenv("PORTS", "8080,8081,8082") @@ -185,7 +188,7 @@ 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: +If `Options.NameSep` is not empty, it is used as the separator. ```go os.Setenv("DB_HOST", "localhost") @@ -216,7 +219,7 @@ type Source interface { } ``` -Here's an example of using `Map`, a `Source` implementation useful in tests: +Here's an example of using `Map`, a `Source` implementation useful in tests. ```go m := env.Map{"PORT": "8080"} @@ -234,7 +237,7 @@ fmt.Println(cfg.Port) // 8080 ### Usage message The `Usage` function prints a usage message documenting all defined environment variables. -An optional usage string can be added to environment variables using the `usage:"STRING"` struct tag: +An optional usage string can be added to environment variables with the `usage:"STRING"` struct tag. ```go os.Unsetenv("DB_HOST") @@ -261,7 +264,7 @@ Usage: HTTP_PORT int default 8080 http server port ``` -The format of the message can be customized by implementing the `Usage([]env.Var, io.Writer, *env.Options)` method: +The format of the message can be customized by implementing the `Usage([]env.Var, io.Writer, *env.Options)` method. ```go type Config struct{ ... } diff --git a/env.go b/env.go index e0dd0f9..717e31f 100644 --- a/env.go +++ b/env.go @@ -15,9 +15,9 @@ type Options struct { 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. +// NotSetError is returned when required environment variables are not set. type NotSetError struct { - Names []string // The names of the missing environment variables. + Names []string } // Error implements the error interface. @@ -51,9 +51,8 @@ func (e *NotSetError) Error() string { // // 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. +// If a nested struct has the optional `env:"PREFIX"` tag, +// the environment variables declared by its fields are prefixed with PREFIX. // // Default values can be specified using the `default:"VALUE"` struct tag. // @@ -126,12 +125,11 @@ func parseVars(v reflect.Value, opts *Options) []Var { continue } - // special case: a nested struct, parse its fields recursively. + tags := v.Type().Field(i).Tag + if kindOf(field, reflect.Struct) && !implements(field, unmarshalerIface) { var prefix string - sf := v.Type().Field(i) - value, ok := sf.Tag.Lookup("env") - if ok { + if value, ok := tags.Lookup("env"); ok { prefix = value + opts.NameSep } for _, v := range parseVars(field, opts) { @@ -141,8 +139,7 @@ func parseVars(v reflect.Value, opts *Options) []Var { continue } - sf := v.Type().Field(i) - value, ok := sf.Tag.Lookup("env") + value, ok := tags.Lookup("env") if !ok { continue } @@ -165,7 +162,7 @@ func parseVars(v reflect.Value, opts *Options) []Var { } } - defValue, defSet := sf.Tag.Lookup("default") + defValue, defSet := tags.Lookup("default") switch { case defSet && required: panic("env: `required` and `default` can't be used simultaneously") @@ -176,7 +173,7 @@ func parseVars(v reflect.Value, opts *Options) []Var { vars = append(vars, Var{ Name: name, Type: field.Type(), - Usage: sf.Tag.Get("usage"), + Usage: tags.Get("usage"), Default: defValue, Required: required, Expand: expand, diff --git a/env_test.go b/env_test.go index 74a5061..7b596af 100644 --- a/env_test.go +++ b/env_test.go @@ -28,7 +28,7 @@ func TestLoad(t *testing.T) { t.Run(name, func(t *testing.T) { const panicMsg = "env: cfg must be a non-nil struct pointer" - load := func() { _ = env.Load(cfg, &env.Options{Source: env.Map{}}) } + load := func() { _ = env.Load(cfg, nil) } assert.Panics[E](t, load, panicMsg) usage := func() { env.Usage(cfg, io.Discard, nil) } @@ -41,7 +41,7 @@ func TestLoad(t *testing.T) { var cfg struct { Foo int `env:""` } - load := func() { _ = env.Load(&cfg, &env.Options{Source: env.Map{}}) } + load := func() { _ = env.Load(&cfg, nil) } assert.Panics[E](t, load, "env: empty tag name is not allowed") }) @@ -49,7 +49,7 @@ func TestLoad(t *testing.T) { var cfg struct { Foo int `env:"FOO,?"` } - load := func() { _ = env.Load(&cfg, &env.Options{Source: env.Map{}}) } + load := func() { _ = env.Load(&cfg, nil) } assert.Panics[E](t, load, "env: invalid tag option `?`") }) @@ -57,7 +57,7 @@ func TestLoad(t *testing.T) { var cfg struct { Foo int `env:"FOO,required" default:"1"` } - load := func() { _ = env.Load(&cfg, &env.Options{Source: env.Map{}}) } + load := func() { _ = env.Load(&cfg, nil) } assert.Panics[E](t, load, "env: `required` and `default` can't be used simultaneously") }) diff --git a/example_test.go b/example_test.go index aa1f4d9..a586410 100644 --- a/example_test.go +++ b/example_test.go @@ -37,30 +37,35 @@ func ExampleLoad_defaultValue() { } func ExampleLoad_nestedStruct() { - os.Setenv("HTTP_PORT", "8080") + os.Setenv("DB_HOST", "localhost") + os.Setenv("DB_PORT", "5432") var cfg struct { - HTTP struct { - Port int `env:"HTTP_PORT"` + DB struct { + Host string `env:"DB_HOST"` + Port int `env:"DB_PORT"` } } if err := env.Load(&cfg, nil); err != nil { fmt.Println(err) } - fmt.Println(cfg.HTTP.Port) - // Output: 8080 + fmt.Println(cfg.DB.Host) + fmt.Println(cfg.DB.Port) + // Output: + // localhost + // 5432 } func ExampleLoad_nestedStructWithPrefix() { - os.Setenv("DBHOST", "localhost") - os.Setenv("DBPORT", "5432") + 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 { fmt.Println(err) diff --git a/usage.go b/usage.go index 010179a..52ed8c2 100644 --- a/usage.go +++ b/usage.go @@ -8,7 +8,7 @@ import ( ) // cache maps a struct type to the [Var] slice parsed from it. -// It is primarily needed to fix the following bug: +// It is needed to fix the following bug: // // var cfg struct { // Port int `env:"PORT"` @@ -16,7 +16,7 @@ import ( // env.Load(&cfg, nil) // 1. sets cfg.Port to 8080 // env.Usage(&cfg, os.Stdout, nil) // 2. prints cfg.Port's default == 8080 (instead of 0) // -// It also speeds up [Usage], since there is no need to parse the struct again. +// It also speeds up [Usage] a bit, since a struct is only parsed once. var cache = make(map[reflect.Type][]Var) // Var holds the information about the environment variable parsed from a struct field. @@ -34,7 +34,7 @@ type Var struct { // Usage writes a usage message documenting all defined environment variables to the given [io.Writer]. // The caller must pass the same [Options] to both [Load] and [Usage], or nil. -// An optional usage string can be added to environment variables using the `usage:"STRING"` struct tag. +// An optional usage string can be added to environment variables with the `usage:"STRING"` struct tag. // The format of the message can be customized by implementing the Usage([]env.Var, io.Writer, *env.Options) method on the cfg's type. func Usage(cfg any, w io.Writer, opts *Options) { pv := reflect.ValueOf(cfg)