Skip to content

Commit ef20794

Browse files
authored
refactor!: reimplement Usage as a separate function (#33)
1 parent ab06c77 commit ef20794

File tree

6 files changed

+121
-166
lines changed

6 files changed

+121
-166
lines changed

README.md

Lines changed: 44 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ go get go-simpler.org/env
3131
* Dependency-free
3232
* Per-variable options: [required](#required), [expand](#expand)
3333
* Global options: [source](#source), [prefix](#prefix), [slice separator](#slice-separator)
34-
* Auto-generated [usage message](#usage-on-error)
34+
* Auto-generated [usage message](#usage-message)
3535

3636
## 📋 Usage
3737

@@ -48,10 +48,11 @@ var cfg struct {
4848
Port int `env:"PORT"`
4949
}
5050
if err := env.Load(&cfg); err != nil {
51-
// handle error
51+
fmt.Println(err)
5252
}
5353

54-
fmt.Println(cfg.Port) // 8080
54+
fmt.Println(cfg.Port)
55+
// Output: 8080
5556
```
5657

5758
### Supported types
@@ -78,34 +79,31 @@ var cfg struct {
7879
}
7980
}
8081
if err := env.Load(&cfg); err != nil {
81-
// handle error
82+
fmt.Println(err)
8283
}
8384

84-
fmt.Println(cfg.HTTP.Port) // 8080
85+
fmt.Println(cfg.HTTP.Port)
86+
// Output: 8080
8587
```
8688

8789
### Default values
8890

89-
Default values can be specified either using the `default` struct tag (has a
90-
higher priority) or by initializing the struct fields directly.
91+
Default values can be specified using the `default` struct tag:
9192

9293
```go
93-
cfg := struct {
94-
Host string `env:"HOST" default:"localhost"` // either use the `default` tag...
95-
Port int `env:"PORT"`
96-
}{
97-
Port: 8080, // ...or initialize the struct field directly.
94+
os.Unsetenv("PORT")
95+
96+
var cfg struct {
97+
Port int `env:"PORT" default:"8080"`
9898
}
9999
if err := env.Load(&cfg); err != nil {
100-
// handle error
100+
fmt.Println(err)
101101
}
102102

103-
fmt.Println(cfg.Host) // localhost
104-
fmt.Println(cfg.Port) // 8080
103+
fmt.Println(cfg.Port)
104+
// Output: 8080
105105
```
106106

107-
## 🔧 Options
108-
109107
### Per-variable options
110108

111109
The name of the environment variable can be followed by comma-separated options
@@ -117,8 +115,8 @@ Use the `required` option to mark the environment variable as required. In case
117115
no such variable is found, an error of type `NotSetError` will be returned.
118116

119117
```go
120-
// os.Setenv("HOST", "localhost")
121-
// os.Setenv("PORT", "8080")
118+
os.Unsetenv("HOST")
119+
os.Unsetenv("PORT")
122120

123121
var cfg struct {
124122
Host string `env:"HOST,required"`
@@ -127,9 +125,11 @@ var cfg struct {
127125
if err := env.Load(&cfg); err != nil {
128126
var notSetErr *env.NotSetError
129127
if errors.As(err, &notSetErr) {
130-
fmt.Println(notSetErr.Names) // [HOST PORT]
128+
fmt.Println(notSetErr.Names)
131129
}
132130
}
131+
132+
// Output: [HOST PORT]
133133
```
134134

135135
#### Expand
@@ -142,13 +142,14 @@ os.Setenv("PORT", "8080")
142142
os.Setenv("ADDR", "localhost:${PORT}")
143143

144144
var cfg struct {
145-
Addr string `env:"ADDR,expand"`
145+
Addr string `env:"ADDR,expand"`
146146
}
147147
if err := env.Load(&cfg); err != nil {
148-
// handle error
148+
fmt.Println(err)
149149
}
150150

151-
fmt.Println(cfg.Addr) // localhost:8080
151+
fmt.Println(cfg.Addr)
152+
// Output: localhost:8080
152153
```
153154

154155
### Global options
@@ -177,10 +178,11 @@ var cfg struct {
177178
Port int `env:"PORT"`
178179
}
179180
if err := env.Load(&cfg, env.WithSource(m)); err != nil {
180-
// handle error
181+
fmt.Println(err)
181182
}
182183

183-
fmt.Println(cfg.Port) // 8080
184+
fmt.Println(cfg.Port)
185+
// Output: 8080
184186
```
185187

186188
#### Prefix
@@ -195,10 +197,11 @@ var cfg struct {
195197
Port int `env:"PORT"`
196198
}
197199
if err := env.Load(&cfg, env.WithPrefix("APP_")); err != nil {
198-
// handle error
200+
fmt.Println(err)
199201
}
200202

201-
fmt.Println(cfg.Port) // 8080
203+
fmt.Println(cfg.Port)
204+
// Output: 8080
202205
```
203206

204207
#### Slice separator
@@ -213,45 +216,35 @@ var cfg struct {
213216
Ports []int `env:"PORTS"`
214217
}
215218
if err := env.Load(&cfg, env.WithSliceSeparator(";")); err != nil {
216-
// handle error
219+
fmt.Println(err)
217220
}
218221

219-
fmt.Println(cfg.Ports[0]) // 8080
220-
fmt.Println(cfg.Ports[1]) // 8081
221-
fmt.Println(cfg.Ports[2]) // 8082
222+
fmt.Println(cfg.Ports)
223+
// Output: [8080 8081 8082]
222224
```
223225

224-
#### Usage on error
226+
### Usage message
225227

226-
`env` supports printing an auto-generated usage message the same way the `flag`
227-
package does it. It will be printed if the `WithUsageOnError` option is
228-
provided and an error occurs while loading environment variables:
228+
`env` supports printing an auto-generated usage message the same way the `flag` package does it.
229229

230230
```go
231-
// os.Setenv("DB_HOST", "localhost")
232-
// os.Setenv("DB_PORT", "5432")
233-
234-
cfg := struct {
231+
var cfg struct {
235232
DB struct {
236233
Host string `env:"DB_HOST,required" desc:"database host"`
237234
Port int `env:"DB_PORT,required" desc:"database port"`
238235
}
239-
HTTPPort int `env:"HTTP_PORT" desc:"http server port"`
240-
Timeouts []time.Duration `env:"TIMEOUTS" desc:"timeout steps"`
241-
}{
242-
HTTPPort: 8080,
243-
Timeouts: []time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second},
236+
HTTPPort int `env:"HTTP_PORT" default:"8080" desc:"http server port"`
244237
}
245-
if err := env.Load(&cfg, env.WithUsageOnError(os.Stdout)); err != nil {
246-
// handle error
238+
if err := env.Load(&cfg); err != nil {
239+
fmt.Println(err)
240+
env.Usage(&cfg, os.Stdout)
247241
}
248242

249-
// Output:
243+
// Output: env: [DB_HOST DB_PORT] are required but not set
250244
// Usage:
251-
// DB_HOST string required database host
252-
// DB_PORT int required database port
253-
// HTTP_PORT int default 8080 http server port
254-
// TIMEOUTS []time.Duration default [1s 2s 3s] timeout steps
245+
// DB_HOST string required database host
246+
// DB_PORT int required database port
247+
// HTTP_PORT int default 8080 http server port
255248
```
256249

257250
[1]: https://12factor.net/config

env.go

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package env
33

44
import (
55
"fmt"
6-
"io"
76
"os"
87
"reflect"
98
"strings"
@@ -76,13 +75,6 @@ func WithSliceSeparator(sep string) Option {
7675
return func(l *loader) { l.sliceSep = sep }
7776
}
7877

79-
// WithUsageOnError configures [Load] to write an auto-generated usage message to the provided [io.Writer],
80-
// if an error occurs while loading environment variables.
81-
// The message format can be changed by assigning the global [Usage] variable to a custom implementation.
82-
func WithUsageOnError(w io.Writer) Option {
83-
return func(l *loader) { l.usageOutput = w }
84-
}
85-
8678
// NotSetError is returned when environment variables are marked as required but not set.
8779
type NotSetError struct {
8880
// Names is a slice of the names of the missing required environment variables.
@@ -95,37 +87,30 @@ func (e *NotSetError) Error() string {
9587
}
9688

9789
type loader struct {
98-
source Source
99-
prefix string
100-
sliceSep string
101-
usageOutput io.Writer
90+
source Source
91+
prefix string
92+
sliceSep string
10293
}
10394

10495
func newLoader(opts []Option) *loader {
10596
l := loader{
106-
source: OS,
107-
prefix: "",
108-
sliceSep: " ",
109-
usageOutput: nil,
97+
source: OS,
98+
prefix: "",
99+
sliceSep: " ",
110100
}
111101
for _, opt := range opts {
112102
opt(&l)
113103
}
114104
return &l
115105
}
116106

117-
func (l *loader) loadVars(cfg any) (err error) {
107+
func (l *loader) loadVars(cfg any) error {
118108
v := reflect.ValueOf(cfg)
119109
if !structPtr(v) {
120-
panic("env: argument must be a non-nil struct pointer")
110+
panic("env: cfg must be a non-nil struct pointer")
121111
}
122112

123113
vars := l.parseVars(v.Elem())
124-
defer func() {
125-
if err != nil && l.usageOutput != nil {
126-
Usage(l.usageOutput, vars)
127-
}
128-
}()
129114

130115
// accumulate missing required variables to return NotSetError after the loop is finished.
131116
var notset []string
@@ -142,6 +127,7 @@ func (l *loader) loadVars(cfg any) (err error) {
142127
value = v.Default
143128
}
144129

130+
var err error
145131
if kindOf(v.field, reflect.Slice) && !implements(v.field, unmarshalerIface) {
146132
err = setSlice(v.field, strings.Split(value, l.sliceSep))
147133
} else {

env_test.go

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ func TestLoad(t *testing.T) {
1919
t.Run("invalid argument", func(t *testing.T) {
2020
test := func(name string, cfg any) {
2121
t.Run(name, func(t *testing.T) {
22-
assert.Panics[E](t,
23-
func() { _ = env.Load(cfg, env.WithSource(env.Map{})) },
24-
"env: argument must be a non-nil struct pointer",
25-
)
22+
const v = "env: cfg must be a non-nil struct pointer"
23+
assert.Panics[E](t, func() { _ = env.Load(cfg, env.WithSource(env.Map{})) }, v)
24+
assert.Panics[E](t, func() { env.Usage(cfg, io.Discard) }, v)
2625
})
2726
}
2827

@@ -184,23 +183,6 @@ func TestLoad(t *testing.T) {
184183
assert.Equal[E](t, cfg.Ports, []int{8080, 8081, 8082})
185184
})
186185

187-
t.Run("with usage on error", func(t *testing.T) {
188-
usage := env.Usage
189-
defer func() { env.Usage = usage }()
190-
191-
var called bool
192-
env.Usage = func(w io.Writer, vars []env.Var) {
193-
called = true
194-
}
195-
196-
var cfg struct {
197-
Port int `env:"PORT,required"`
198-
}
199-
err := env.Load(&cfg, env.WithSource(env.Map{}), env.WithUsageOnError(io.Discard))
200-
assert.AsErr[F](t, err, new(*env.NotSetError))
201-
assert.Equal[E](t, called, true)
202-
})
203-
204186
t.Run("all supported types", func(t *testing.T) {
205187
m := env.Map{
206188
"INT": "-1", "INTS": "-1 0",

0 commit comments

Comments
 (0)