Skip to content

Commit 5c9e13c

Browse files
committed
Optionally propagate context to Decoder
This introduces a new way to define `EnvDecode` to optionally pass in a context. The old way still works, but the context method is preferred. Since it's actually impossible to define the same function with different arguments on the same receiver, there's no possibility of conflict.
1 parent db7a7a5 commit 5c9e13c

File tree

3 files changed

+159
-17
lines changed

3 files changed

+159
-17
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,21 @@ the non-nil value. To change this behavior, see
290290

291291
### Custom Decoders
292292

293-
You can also define your own decoders. See the [godoc][godoc] for more
294-
information.
293+
You can also define your own decoders.
294+
295+
```go
296+
type MyCustomType struct {
297+
value string
298+
}
299+
300+
func (t *MyCustomType) EnvDecode(ctx context.Context, val string) error {
301+
resolved := someComplexFunction(val)
302+
t.value = resolved
303+
return nil
304+
}
305+
```
306+
307+
See the [godoc][godoc] for more information.
295308

296309

297310
## Testing

envconfig.go

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -214,16 +214,23 @@ type keyedLookuper interface {
214214
Key(key string) string
215215
}
216216

217-
// Decoder is an interface that custom types/fields can implement to control how
218-
// decoding takes place. For example:
217+
// Decoder is the legacy implementation of [DecoderCtx], but it does not accept
218+
// a context as the first parameter to `EnvDecode`. Please use [DecoderCtx]
219+
// instead, as this will be removed in a future release.
220+
type Decoder interface {
221+
EnvDecode(val string) error
222+
}
223+
224+
// DecoderCtx is an interface that custom types/fields can implement to control
225+
// how decoding takes place. For example:
219226
//
220227
// type MyType string
221228
//
222-
// func (mt MyType) EnvDecode(val string) error {
229+
// func (mt MyType) EnvDecode(ctx context.Context, val string) error {
223230
// return "CUSTOM-"+val
224231
// }
225-
type Decoder interface {
226-
EnvDecode(val string) error
232+
type DecoderCtx interface {
233+
EnvDecode(ctx context.Context, val string) error
227234
}
228235

229236
// options are internal options for decoding.
@@ -471,7 +478,7 @@ func processWith(ctx context.Context, c *Config) error {
471478
}
472479

473480
if found || usedDefault || decodeUnset {
474-
if ok, err := processAsDecoder(val, ef); ok {
481+
if ok, err := processAsDecoder(ctx, val, ef); ok {
475482
if err != nil {
476483
return err
477484
}
@@ -556,7 +563,7 @@ func processWith(ctx context.Context, c *Config) error {
556563
}
557564

558565
// Set value.
559-
if err := processField(val, ef, delimiter, separator, noInit); err != nil {
566+
if err := processField(ctx, val, ef, delimiter, separator, noInit); err != nil {
560567
return fmt.Errorf("%s: %w", tf.Name, err)
561568
}
562569
}
@@ -692,7 +699,7 @@ func lookup(key string, required bool, defaultValue string, l Lookuper) (string,
692699

693700
// processAsDecoder processes the given value as a decoder or custom
694701
// unmarshaller.
695-
func processAsDecoder(v string, ef reflect.Value) (bool, error) {
702+
func processAsDecoder(ctx context.Context, v string, ef reflect.Value) (bool, error) {
696703
// Keep a running error. It's possible that a property might implement
697704
// multiple decoders, and we don't know *which* decoder will succeed. If we
698705
// get through all of them, we'll return the most recent error.
@@ -711,6 +718,13 @@ func processAsDecoder(v string, ef reflect.Value) (bool, error) {
711718
// never attempt to use other decoders in case of failure. EnvDecode's
712719
// decoding logic is "the right one", and the error returned (if any)
713720
// is the most specific we can get.
721+
if dec, ok := iface.(DecoderCtx); ok {
722+
imp = true
723+
err = dec.EnvDecode(ctx, v)
724+
return imp, err
725+
}
726+
727+
// Check legacy decoder implementation
714728
if dec, ok := iface.(Decoder); ok {
715729
imp = true
716730
err = dec.EnvDecode(v)
@@ -749,7 +763,7 @@ func processAsDecoder(v string, ef reflect.Value) (bool, error) {
749763
return imp, err
750764
}
751765

752-
func processField(v string, ef reflect.Value, delimiter, separator string, noInit bool) error {
766+
func processField(ctx context.Context, v string, ef reflect.Value, delimiter, separator string, noInit bool) error {
753767
// If the input value is empty and initialization is skipped, do nothing.
754768
if v == "" && noInit {
755769
return nil
@@ -767,7 +781,7 @@ func processField(v string, ef reflect.Value, delimiter, separator string, noIni
767781
tk := tf.Kind()
768782

769783
// Handle existing decoders.
770-
if ok, err := processAsDecoder(v, ef); ok {
784+
if ok, err := processAsDecoder(ctx, v, ef); ok {
771785
return err
772786
}
773787

@@ -837,12 +851,12 @@ func processField(v string, ef reflect.Value, delimiter, separator string, noIni
837851
mKey, mVal := strings.TrimSpace(pair[0]), strings.TrimSpace(pair[1])
838852

839853
k := reflect.New(tf.Key()).Elem()
840-
if err := processField(mKey, k, delimiter, separator, noInit); err != nil {
854+
if err := processField(ctx, mKey, k, delimiter, separator, noInit); err != nil {
841855
return fmt.Errorf("%s: %w", mKey, err)
842856
}
843857

844858
v := reflect.New(tf.Elem()).Elem()
845-
if err := processField(mVal, v, delimiter, separator, noInit); err != nil {
859+
if err := processField(ctx, mVal, v, delimiter, separator, noInit); err != nil {
846860
return fmt.Errorf("%s: %w", mVal, err)
847861
}
848862

@@ -860,7 +874,7 @@ func processField(v string, ef reflect.Value, delimiter, separator string, noIni
860874
s := reflect.MakeSlice(tf, len(vals), len(vals))
861875
for i, val := range vals {
862876
val = strings.TrimSpace(val)
863-
if err := processField(val, s.Index(i), delimiter, separator, noInit); err != nil {
877+
if err := processField(ctx, val, s.Index(i), delimiter, separator, noInit); err != nil {
864878
return fmt.Errorf("%s: %w", val, err)
865879
}
866880
}

envconfig_test.go

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,26 @@ import (
3131
"github.com/google/go-cmp/cmp"
3232
)
3333

34-
var _ Decoder = (*CustomDecoderType)(nil)
34+
var _ DecoderCtx = (*CustomDecoderType)(nil)
3535

3636
// CustomDecoderType is used to test custom decoding using Decoder.
3737
type CustomDecoderType struct {
3838
value string
3939
}
4040

41-
func (c *CustomDecoderType) EnvDecode(val string) error {
41+
func (c *CustomDecoderType) EnvDecode(ctx context.Context, val string) error {
42+
c.value = "CUSTOM-" + val
43+
return nil
44+
}
45+
46+
var _ Decoder = (*CustomDecoderTypeLegacy)(nil)
47+
48+
// CustomDecoderTypeLegacy is used to test custom decoding using Decoder.
49+
type CustomDecoderTypeLegacy struct {
50+
value string
51+
}
52+
53+
func (c *CustomDecoderTypeLegacy) EnvDecode(val string) error {
4254
c.value = "CUSTOM-" + val
4355
return nil
4456
}
@@ -1428,6 +1440,14 @@ func TestProcessWith(t *testing.T) {
14281440
},
14291441

14301442
// Syntax
1443+
{
1444+
name: "syntax/=key",
1445+
target: &struct {
1446+
Field CustomDecoderTypeLegacy `env:"FIELD=foo"`
1447+
}{},
1448+
lookuper: MapLookuper(nil),
1449+
err: ErrInvalidEnvvarName,
1450+
},
14311451
{
14321452
name: "syntax/=key",
14331453
target: &struct {
@@ -1619,6 +1639,98 @@ func TestProcessWith(t *testing.T) {
16191639
lookuper: MapLookuper(nil),
16201640
},
16211641

1642+
// Custom decoder - legacy
1643+
{
1644+
name: "custom_decoder/struct",
1645+
target: &struct {
1646+
Field CustomDecoderTypeLegacy `env:"FIELD"`
1647+
}{},
1648+
exp: &struct {
1649+
Field CustomDecoderTypeLegacy `env:"FIELD"`
1650+
}{
1651+
Field: CustomDecoderTypeLegacy{
1652+
value: "CUSTOM-foo",
1653+
},
1654+
},
1655+
lookuper: MapLookuper(map[string]string{
1656+
"FIELD": "foo",
1657+
}),
1658+
},
1659+
{
1660+
name: "custom_decoder/pointer",
1661+
target: &struct {
1662+
Field *CustomDecoderTypeLegacy `env:"FIELD"`
1663+
}{},
1664+
exp: &struct {
1665+
Field *CustomDecoderTypeLegacy `env:"FIELD"`
1666+
}{
1667+
Field: &CustomDecoderTypeLegacy{
1668+
value: "CUSTOM-foo",
1669+
},
1670+
},
1671+
lookuper: MapLookuper(map[string]string{
1672+
"FIELD": "foo",
1673+
}),
1674+
},
1675+
{
1676+
name: "custom_decoder/private",
1677+
target: &struct {
1678+
field *CustomDecoderTypeLegacy `env:"FIELD"`
1679+
}{},
1680+
lookuper: MapLookuper(map[string]string{
1681+
"FIELD": "foo",
1682+
}),
1683+
err: ErrPrivateField,
1684+
},
1685+
{
1686+
name: "custom_decoder/error",
1687+
target: &struct {
1688+
Field CustomTypeError `env:"FIELD"`
1689+
}{},
1690+
lookuper: MapLookuper(map[string]string{
1691+
"FIELD": "foo",
1692+
}),
1693+
errMsg: "broken",
1694+
},
1695+
{
1696+
name: "custom_decoder/called_for_empty_string",
1697+
target: &struct {
1698+
Field CustomTypeError `env:"FIELD"`
1699+
}{},
1700+
lookuper: MapLookuper(map[string]string{
1701+
"FIELD": "",
1702+
}),
1703+
errMsg: "broken",
1704+
},
1705+
{
1706+
name: "custom_decoder/called_when_default",
1707+
target: &struct {
1708+
Field *CustomDecoderTypeLegacy `env:"FIELD, default=foo"`
1709+
}{},
1710+
exp: &struct {
1711+
Field *CustomDecoderTypeLegacy `env:"FIELD, default=foo"`
1712+
}{
1713+
Field: &CustomDecoderTypeLegacy{
1714+
value: "CUSTOM-foo",
1715+
},
1716+
},
1717+
lookuper: MapLookuper(nil),
1718+
},
1719+
{
1720+
name: "custom_decoder/called_on_decodeunset",
1721+
target: &struct {
1722+
Field *CustomDecoderTypeLegacy `env:"FIELD, decodeunset"`
1723+
}{},
1724+
exp: &struct {
1725+
Field *CustomDecoderTypeLegacy `env:"FIELD, decodeunset"`
1726+
}{
1727+
Field: &CustomDecoderTypeLegacy{
1728+
value: "CUSTOM-",
1729+
},
1730+
},
1731+
lookuper: MapLookuper(nil),
1732+
},
1733+
16221734
// Expand
16231735
{
16241736
name: "expand/not_default",
@@ -3034,6 +3146,9 @@ func TestProcessWith(t *testing.T) {
30343146
// Custom decoder type
30353147
CustomDecoderType{},
30363148

3149+
// Custom decoder type - legacy
3150+
CustomDecoderTypeLegacy{},
3151+
30373152
// Custom standard library interfaces decoder type
30383153
CustomStdLibDecodingType{},
30393154

0 commit comments

Comments
 (0)