Skip to content

Commit ab06c77

Browse files
authored
refactor!: replace LoadFrom and MultiSource with WithSource (#35)
1 parent b6844e3 commit ab06c77

File tree

7 files changed

+108
-156
lines changed

7 files changed

+108
-156
lines changed

README.md

Lines changed: 31 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ go get go-simpler.org/env
2929

3030
* Simple API
3131
* Dependency-free
32-
* Custom [sources](#source)
3332
* Per-variable options: [required](#required), [expand](#expand)
34-
* Global options: [prefix](#prefix), [slice separator](#slice-separator)
33+
* Global options: [source](#source), [prefix](#prefix), [slice separator](#slice-separator)
3534
* Auto-generated [usage message](#usage-on-error)
3635

3736
## 📋 Usage
@@ -105,60 +104,7 @@ fmt.Println(cfg.Host) // localhost
105104
fmt.Println(cfg.Port) // 8080
106105
```
107106

108-
## 🔧 Configuration
109-
110-
### Source
111-
112-
`Load` retrieves environment variables values directly from OS. To use a
113-
different source, try `LoadFrom` that accepts an implementation of the
114-
`Source` interface as the first argument.
115-
116-
```go
117-
// Source represents a source of environment variables.
118-
type Source interface {
119-
// LookupEnv retrieves the value of the environment variable named by the key.
120-
LookupEnv(key string) (value string, ok bool)
121-
}
122-
```
123-
124-
`Map` is a built-in `Source` implementation that might be useful in tests.
125-
126-
```go
127-
m := env.Map{"PORT": "8080"}
128-
129-
var cfg struct {
130-
Port int `env:"PORT"`
131-
}
132-
if err := env.LoadFrom(m, &cfg); err != nil {
133-
// handle error
134-
}
135-
136-
fmt.Println(cfg.Port) // 8080
137-
```
138-
139-
Multiple sources can be combined into a single one using `MultiSource`. The
140-
order of the given sources matters: if the same key is found in several
141-
sources, the value from the last one takes the precedence.
142-
143-
```go
144-
os.Setenv("HOST", "localhost")
145-
146-
src := env.MultiSource(
147-
env.OS,
148-
env.Map{"PORT": "8080"},
149-
)
150-
151-
var cfg struct {
152-
Host string `env:"HOST,required"`
153-
Port int `env:"PORT,required"`
154-
}
155-
if err := env.LoadFrom(src, &cfg); err != nil {
156-
// handle error
157-
}
158-
159-
fmt.Println(cfg.Host) // localhost
160-
fmt.Println(cfg.Port) // 8080
161-
```
107+
## 🔧 Options
162108

163109
### Per-variable options
164110

@@ -207,8 +153,35 @@ fmt.Println(cfg.Addr) // localhost:8080
207153

208154
### Global options
209155

210-
In addition to the per-variable options, `env` also supports global options that
211-
apply to all variables.
156+
`Load` also accepts global options that apply to all environment variables.
157+
158+
#### Source
159+
160+
By default, `Load` retrieves environment variables values directly from OS.
161+
To use a different source, provide an implementation of the `Source` interface via the `WithSource` option.
162+
163+
```go
164+
// Source represents a source of environment variables.
165+
type Source interface {
166+
// LookupEnv retrieves the value of the environment variable named by the key.
167+
LookupEnv(key string) (value string, ok bool)
168+
}
169+
```
170+
171+
Here's an example of using `Map`, a builtin `Source` implementation useful in tests:
172+
173+
```go
174+
m := env.Map{"PORT": "8080"}
175+
176+
var cfg struct {
177+
Port int `env:"PORT"`
178+
}
179+
if err := env.Load(&cfg, env.WithSource(m)); err != nil {
180+
// handle error
181+
}
182+
183+
fmt.Println(cfg.Port) // 8080
184+
```
212185

213186
#### Prefix
214187

env.go

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
)
1111

1212
// Load loads environment variables into the provided struct using the [OS] [Source].
13-
// To specify a custom [Source], use the [LoadFrom] function.
1413
// cfg must be a non-nil struct pointer, otherwise Load panics.
1514
//
1615
// The struct fields must have the `env:"VAR"` struct tag, where VAR is the name of the corresponding environment variable.
@@ -45,39 +44,39 @@ import (
4544
//
4645
// # Global options
4746
//
48-
// In addition to the per-variable options, [env] also supports global options that apply to all variables:
49-
//
50-
// - [WithPrefix]: sets prefix for each environment variable
51-
// - [WithSliceSeparator]: sets custom separator to parse slice values
52-
// - [WithUsageOnError]: enables a usage message printing when an error occurs
53-
//
54-
// See their documentation for details.
47+
// Load also accepts global options that apply to all environment variables, see the With* functions for details.
5548
func Load(cfg any, opts ...Option) error {
56-
return newLoader(OS, opts...).loadVars(cfg)
57-
}
58-
59-
// LoadFrom loads environment variables into the provided struct using the specified [Source].
60-
// See [Load] documentation for more details.
61-
func LoadFrom(src Source, cfg any, opts ...Option) error {
62-
return newLoader(src, opts...).loadVars(cfg)
49+
return newLoader(opts).loadVars(cfg)
6350
}
6451

65-
// Option allows to configure the behaviour of the [Load]/[LoadFrom] functions.
52+
// Option allows to configure the behaviour of the [Load] function.
6653
type Option func(*loader)
6754

68-
// WithPrefix configures [Load]/[LoadFrom] to automatically add the provided prefix to each environment variable.
55+
// WithSource configures [Load] to retrieve environment variables from the provided [Source].
56+
// If multiple sources are provided, they will be merged into a single one containing the union of all environment variables.
57+
// The order of the sources matters: if the same key occurs more than once, the later value takes precedence.
58+
// The default source is [OS].
59+
func WithSource(src Source, srcs ...Source) Option {
60+
// reverse the slice first, since the later value should take precedence.
61+
for i, j := 0, len(srcs)-1; i < j; i, j = i+1, j-1 {
62+
srcs[i], srcs[j] = srcs[j], srcs[i]
63+
}
64+
return func(l *loader) { l.source = multiSource(append(srcs, src)) }
65+
}
66+
67+
// WithPrefix configures [Load] to automatically add the provided prefix to each environment variable.
6968
// By default, no prefix is configured.
7069
func WithPrefix(prefix string) Option {
7170
return func(l *loader) { l.prefix = prefix }
7271
}
7372

74-
// WithSliceSeparator configures [Load]/[LoadFrom] to use the provided separator when parsing slice values.
75-
// The default one is space.
73+
// WithSliceSeparator configures [Load] to use the provided separator when parsing slice values.
74+
// The default separator is space.
7675
func WithSliceSeparator(sep string) Option {
7776
return func(l *loader) { l.sliceSep = sep }
7877
}
7978

80-
// WithUsageOnError configures [Load]/[LoadFrom] to write an auto-generated usage message to the provided [io.Writer],
79+
// WithUsageOnError configures [Load] to write an auto-generated usage message to the provided [io.Writer],
8180
// if an error occurs while loading environment variables.
8281
// The message format can be changed by assigning the global [Usage] variable to a custom implementation.
8382
func WithUsageOnError(w io.Writer) Option {
@@ -102,9 +101,9 @@ type loader struct {
102101
usageOutput io.Writer
103102
}
104103

105-
func newLoader(src Source, opts ...Option) *loader {
104+
func newLoader(opts []Option) *loader {
106105
l := loader{
107-
source: src,
106+
source: OS,
108107
prefix: "",
109108
sliceSep: " ",
110109
usageOutput: nil,

env_test.go

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import (
1515

1616
//go:generate go run -tags=copier go-simpler.org/assert/cmd/copier@v0.6.0 internal
1717

18-
func TestLoadFrom(t *testing.T) {
18+
func TestLoad(t *testing.T) {
1919
t.Run("invalid argument", func(t *testing.T) {
20-
test := func(name string, dst any) {
20+
test := func(name string, cfg any) {
2121
t.Run(name, func(t *testing.T) {
2222
assert.Panics[E](t,
23-
func() { _ = env.LoadFrom(env.Map{}, dst) },
23+
func() { _ = env.Load(cfg, env.WithSource(env.Map{})) },
2424
"env: argument must be a non-nil struct pointer",
2525
)
2626
})
@@ -37,7 +37,7 @@ func TestLoadFrom(t *testing.T) {
3737
Port string `env:""`
3838
}
3939
assert.Panics[E](t,
40-
func() { _ = env.LoadFrom(env.Map{}, &cfg) },
40+
func() { _ = env.Load(&cfg, env.WithSource(env.Map{})) },
4141
"env: empty tag name is not allowed",
4242
)
4343
})
@@ -49,7 +49,7 @@ func TestLoadFrom(t *testing.T) {
4949
Port complex64 `env:"PORT"`
5050
}
5151
assert.Panics[E](t,
52-
func() { _ = env.LoadFrom(m, &cfg) },
52+
func() { _ = env.Load(&cfg, env.WithSource(m)) },
5353
"env: unsupported type `complex64`",
5454
)
5555
})
@@ -64,7 +64,7 @@ func TestLoadFrom(t *testing.T) {
6464
unexported string `env:"UNEXPORTED"`
6565
MissingTag string
6666
}
67-
err := env.LoadFrom(m, &cfg)
67+
err := env.Load(&cfg, env.WithSource(m))
6868
assert.NoErr[F](t, err)
6969
assert.Equal[E](t, cfg.unexported, "")
7070
assert.Equal[E](t, cfg.MissingTag, "")
@@ -77,7 +77,7 @@ func TestLoadFrom(t *testing.T) {
7777
}{
7878
Port: 8000, // must be overridden with 8080 (from the `default` tag).
7979
}
80-
err := env.LoadFrom(env.Map{}, &cfg)
80+
err := env.Load(&cfg, env.WithSource(env.Map{}))
8181
assert.NoErr[F](t, err)
8282
assert.Equal[E](t, cfg.Host, "localhost")
8383
assert.Equal[E](t, cfg.Port, 8080)
@@ -97,7 +97,7 @@ func TestLoadFrom(t *testing.T) {
9797
Port int `env:"HTTP_PORT"`
9898
}
9999
}
100-
err := env.LoadFrom(m, &cfg)
100+
err := env.Load(&cfg, env.WithSource(m))
101101
assert.NoErr[F](t, err)
102102
assert.Equal[E](t, cfg.DB.Port, 5432)
103103
assert.Equal[E](t, cfg.HTTP.Port, 8080)
@@ -110,7 +110,7 @@ func TestLoadFrom(t *testing.T) {
110110
Host string `env:"HOST,required"`
111111
Port int `env:"PORT,required"`
112112
}
113-
err := env.LoadFrom(env.Map{}, &cfg)
113+
err := env.Load(&cfg, env.WithSource(env.Map{}))
114114
assert.AsErr[F](t, err, &notSetErr)
115115
assert.Equal[E](t, notSetErr.Names, []string{"HOST", "PORT"})
116116

@@ -128,7 +128,7 @@ func TestLoadFrom(t *testing.T) {
128128
var cfg struct {
129129
Addr string `env:"ADDR,expand"`
130130
}
131-
err := env.LoadFrom(m, &cfg)
131+
err := env.Load(&cfg, env.WithSource(m))
132132
assert.NoErr[F](t, err)
133133
assert.Equal[E](t, cfg.Addr, "localhost:8080")
134134
})
@@ -140,18 +140,35 @@ func TestLoadFrom(t *testing.T) {
140140
}
141141
}
142142
assert.Panics[E](t,
143-
func() { _ = env.LoadFrom(env.Map{}, &cfg) },
143+
func() { _ = env.Load(&cfg, env.WithSource(env.Map{})) },
144144
"env: invalid tag option `foo`",
145145
)
146146
})
147147

148+
t.Run("with source", func(t *testing.T) {
149+
m1 := env.Map{"FOO": "1", "BAR": "2"}
150+
m2 := env.Map{"FOO": "2", "BAZ": "3"}
151+
m3 := env.Map{"BAR": "3", "BAZ": "4"}
152+
153+
var cfg struct {
154+
Foo int `env:"FOO,required"`
155+
Bar int `env:"BAR,required"`
156+
Baz int `env:"BAZ,required"`
157+
}
158+
err := env.Load(&cfg, env.WithSource(m1, m2, m3))
159+
assert.NoErr[F](t, err)
160+
assert.Equal[E](t, cfg.Foo, 2)
161+
assert.Equal[E](t, cfg.Bar, 3)
162+
assert.Equal[E](t, cfg.Baz, 4)
163+
})
164+
148165
t.Run("with prefix", func(t *testing.T) {
149166
m := env.Map{"APP_PORT": "8080"}
150167

151168
var cfg struct {
152169
Port int `env:"PORT"`
153170
}
154-
err := env.LoadFrom(m, &cfg, env.WithPrefix("APP_"))
171+
err := env.Load(&cfg, env.WithSource(m), env.WithPrefix("APP_"))
155172
assert.NoErr[F](t, err)
156173
assert.Equal[E](t, cfg.Port, 8080)
157174
})
@@ -162,7 +179,7 @@ func TestLoadFrom(t *testing.T) {
162179
var cfg struct {
163180
Ports []int `env:"PORTS"`
164181
}
165-
err := env.LoadFrom(m, &cfg, env.WithSliceSeparator(";"))
182+
err := env.Load(&cfg, env.WithSource(m), env.WithSliceSeparator(";"))
166183
assert.NoErr[F](t, err)
167184
assert.Equal[E](t, cfg.Ports, []int{8080, 8081, 8082})
168185
})
@@ -179,7 +196,7 @@ func TestLoadFrom(t *testing.T) {
179196
var cfg struct {
180197
Port int `env:"PORT,required"`
181198
}
182-
err := env.LoadFrom(env.Map{}, &cfg, env.WithUsageOnError(io.Discard))
199+
err := env.Load(&cfg, env.WithSource(env.Map{}), env.WithUsageOnError(io.Discard))
183200
assert.AsErr[F](t, err, new(*env.NotSetError))
184201
assert.Equal[E](t, called, true)
185202
})
@@ -238,7 +255,7 @@ func TestLoadFrom(t *testing.T) {
238255
IP net.IP `env:"IP"`
239256
IPs []net.IP `env:"IPS"`
240257
}
241-
err := env.LoadFrom(m, &cfg)
258+
err := env.Load(&cfg, env.WithSource(m))
242259
assert.NoErr[F](t, err)
243260

244261
test := func(name string, got, want any) {
@@ -297,7 +314,7 @@ func TestLoadFrom(t *testing.T) {
297314
Unmarshaler net.IP `env:"UNMARSHALER"`
298315
Slice []net.IP `env:"SLICE"`
299316
}
300-
err := env.LoadFrom(m, &cfg)
317+
err := env.Load(&cfg, env.WithSource(m))
301318
assert.Equal[E](t, checkErr(err), true)
302319
})
303320
}

example_test.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,37 +83,35 @@ func ExampleLoad_expand() {
8383
fmt.Println(cfg.Addr) // localhost:8080
8484
}
8585

86-
func ExampleLoadFrom() {
86+
func ExampleWithSource() {
8787
m := env.Map{"PORT": "8080"}
8888

8989
var cfg struct {
9090
Port int `env:"PORT"`
9191
}
92-
if err := env.LoadFrom(m, &cfg); err != nil {
92+
if err := env.Load(&cfg, env.WithSource(m)); err != nil {
9393
// handle error
9494
}
9595

9696
fmt.Println(cfg.Port) // 8080
9797
}
9898

99-
func ExampleMultiSource() {
100-
os.Setenv("HOST", "localhost")
99+
func ExampleWithSource_multiple() {
100+
m := env.Map{"PORT": "8080"}
101101

102-
src := env.MultiSource(
103-
env.OS,
104-
env.Map{"PORT": "8080"},
105-
)
102+
os.Setenv("HOST", "localhost")
103+
os.Setenv("PORT", "8081") // overrides PORT from m.
106104

107105
var cfg struct {
108106
Host string `env:"HOST,required"`
109107
Port int `env:"PORT,required"`
110108
}
111-
if err := env.LoadFrom(src, &cfg); err != nil {
109+
if err := env.Load(&cfg, env.WithSource(m, env.OS)); err != nil {
112110
// handle error
113111
}
114112

115113
fmt.Println(cfg.Host) // localhost
116-
fmt.Println(cfg.Port) // 8080
114+
fmt.Println(cfg.Port) // 8081
117115
}
118116

119117
func ExampleWithPrefix() {

0 commit comments

Comments
 (0)