Skip to content

Commit

Permalink
dev: minor cleanups (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmzane authored May 2, 2024
1 parent d416254 commit b3e5818
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 47 deletions.
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ linters:
- govet
- ineffassign
- staticcheck
- typecheck
- unused
# disabled by default:
- gocritic
Expand Down
39 changes: 21 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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"}
Expand All @@ -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")
Expand All @@ -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{ ... }
Expand Down
23 changes: 10 additions & 13 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
//
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand All @@ -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")
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand All @@ -41,23 +41,23 @@ 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")
})

t.Run("invalid tag option", func(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 `?`")
})

t.Run("required with default", func(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")
})

Expand Down
21 changes: 13 additions & 8 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ 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"`
// }
// 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.
Expand All @@ -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)
Expand Down

0 comments on commit b3e5818

Please sign in to comment.