From 39c9bc7010cc446cf0fb569a3aedfa6b0de8462a Mon Sep 17 00:00:00 2001 From: Joakim Gustin Date: Thu, 9 Jun 2022 15:45:17 +0200 Subject: [PATCH] Change custom unmarshaling order (#59) Since the input parsed by envconfig is most often configuration written by humans, it makes sense to test unmarshaling as text before any other formats. Also JSON should be more common than binary, so this commits sets the order to: 1. envconfig.Decoder 2. encoding.TextUnmarshaler 3. json.Unmarshaler 4. encoding.BinaryUnmarshaler 5. gob.GobDecoder and adds tests to very that order. Closes #58. --- envconfig.go | 16 ++--- envconfig_test.go | 175 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 170 insertions(+), 21 deletions(-) diff --git a/envconfig.go b/envconfig.go index 26ef06c..3719648 100644 --- a/envconfig.go +++ b/envconfig.go @@ -518,30 +518,30 @@ func processAsDecoder(v string, ef reflect.Value) (bool, error) { return imp, err } - if tu, ok := iface.(encoding.BinaryUnmarshaler); ok { + if tu, ok := iface.(encoding.TextUnmarshaler); ok { imp = true - if err = tu.UnmarshalBinary([]byte(v)); err == nil { + if err = tu.UnmarshalText([]byte(v)); err == nil { return imp, nil } } - if tu, ok := iface.(gob.GobDecoder); ok { + if tu, ok := iface.(json.Unmarshaler); ok { imp = true - if err = tu.GobDecode([]byte(v)); err == nil { + if err = tu.UnmarshalJSON([]byte(v)); err == nil { return imp, nil } } - if tu, ok := iface.(json.Unmarshaler); ok { + if tu, ok := iface.(encoding.BinaryUnmarshaler); ok { imp = true - if err = tu.UnmarshalJSON([]byte(v)); err == nil { + if err = tu.UnmarshalBinary([]byte(v)); err == nil { return imp, nil } } - if tu, ok := iface.(encoding.TextUnmarshaler); ok { + if tu, ok := iface.(gob.GobDecoder); ok { imp = true - if err = tu.UnmarshalText([]byte(v)); err == nil { + if err = tu.GobDecode([]byte(v)); err == nil { return imp, nil } } diff --git a/envconfig_test.go b/envconfig_test.go index 82404d7..64fe875 100644 --- a/envconfig_test.go +++ b/envconfig_test.go @@ -30,18 +30,74 @@ import ( "github.com/google/go-cmp/cmp" ) -var _ Decoder = (*CustomType)(nil) +var _ Decoder = (*CustomDecoderType)(nil) -// CustomType is used to test custom decode methods. -type CustomType struct { +// CustomDecoderType is used to test custom decoding using Decoder. +type CustomDecoderType struct { value string } -func (c *CustomType) EnvDecode(val string) error { +func (c *CustomDecoderType) EnvDecode(val string) error { c.value = "CUSTOM-" + val return nil } +var ( + _ encoding.BinaryUnmarshaler = (*CustomStdLibDecodingType)(nil) + _ encoding.TextUnmarshaler = (*CustomStdLibDecodingType)(nil) + _ json.Unmarshaler = (*CustomStdLibDecodingType)(nil) + _ gob.GobDecoder = (*CustomStdLibDecodingType)(nil) +) + +// CustomStdLibDecodingType is used to test custom decoding using the standard +// library custom unmarshaling interfaces. +type CustomStdLibDecodingType struct { + // used to control implementations + implementsTextUnmarshaler bool + implementsBinaryUnmarshaler bool + implementsJSONUnmarshaler bool + implementsGobDecoder bool + + value string +} + +// Equal returns whether the decoded values are equal. +func (c CustomStdLibDecodingType) Equal(c2 CustomStdLibDecodingType) bool { + return c.value == c2.value +} + +func (c *CustomStdLibDecodingType) UnmarshalBinary(data []byte) error { + if !c.implementsBinaryUnmarshaler { + return errors.New("binary unmarshaler not implemented") + } + c.value = "BINARY-" + string(data) + return nil +} + +func (c *CustomStdLibDecodingType) UnmarshalText(text []byte) error { + if !c.implementsTextUnmarshaler { + return errors.New("text unmarshaler not implemented") + } + c.value = "TEXT-" + string(text) + return nil +} + +func (c *CustomStdLibDecodingType) UnmarshalJSON(data []byte) error { + if !c.implementsJSONUnmarshaler { + return errors.New("JSON unmarshaler not implemented") + } + c.value = "JSON-" + string(data) + return nil +} + +func (c *CustomStdLibDecodingType) GobDecode(data []byte) error { + if !c.implementsGobDecoder { + return errors.New("Gob decoder not implemented") + } + c.value = "GOB-" + string(data) + return nil +} + var ( _ Decoder = (*CustomTypeError)(nil) _ encoding.BinaryUnmarshaler = (*CustomTypeError)(nil) @@ -1086,22 +1142,112 @@ func TestProcessWith(t *testing.T) { { name: "syntax/=key", input: &struct { - Field CustomType `env:"FIELD=foo"` + Field CustomDecoderType `env:"FIELD=foo"` }{}, lookuper: MapLookuper(map[string]string{}), err: ErrInvalidEnvvarName, }, + // Custom decoding from standard library interfaces + { + name: "custom_decoder/gob_decoder", + input: &struct { + Field CustomStdLibDecodingType `env:"FIELD"` + }{ + Field: CustomStdLibDecodingType{ + implementsGobDecoder: true, + }, + }, + exp: &struct { + Field CustomStdLibDecodingType `env:"FIELD"` + }{ + Field: CustomStdLibDecodingType{ + value: "GOB-foo", + }, + }, + lookuper: MapLookuper(map[string]string{ + "FIELD": "foo", + }), + }, + { + name: "custom_decoder/binary_unmarshaler", + input: &struct { + Field CustomStdLibDecodingType `env:"FIELD"` + }{ + Field: CustomStdLibDecodingType{ + implementsBinaryUnmarshaler: true, + implementsGobDecoder: true, + }, + }, + exp: &struct { + Field CustomStdLibDecodingType `env:"FIELD"` + }{ + Field: CustomStdLibDecodingType{ + value: "BINARY-foo", + }, + }, + lookuper: MapLookuper(map[string]string{ + "FIELD": "foo", + }), + }, + { + name: "custom_decoder/json_unmarshaler", + input: &struct { + Field CustomStdLibDecodingType `env:"FIELD"` + }{ + Field: CustomStdLibDecodingType{ + implementsBinaryUnmarshaler: true, + implementsJSONUnmarshaler: true, + implementsGobDecoder: true, + }, + }, + exp: &struct { + Field CustomStdLibDecodingType `env:"FIELD"` + }{ + Field: CustomStdLibDecodingType{ + implementsTextUnmarshaler: true, + value: "JSON-foo", + }, + }, + lookuper: MapLookuper(map[string]string{ + "FIELD": "foo", + }), + }, + { + name: "custom_decoder/text_unmarshaler", + input: &struct { + Field CustomStdLibDecodingType `env:"FIELD"` + }{ + Field: CustomStdLibDecodingType{ + implementsTextUnmarshaler: true, + implementsBinaryUnmarshaler: true, + implementsJSONUnmarshaler: true, + implementsGobDecoder: true, + }, + }, + exp: &struct { + Field CustomStdLibDecodingType `env:"FIELD"` + }{ + Field: CustomStdLibDecodingType{ + implementsTextUnmarshaler: true, + value: "TEXT-foo", + }, + }, + lookuper: MapLookuper(map[string]string{ + "FIELD": "foo", + }), + }, + // Custom decoder { name: "custom_decoder/struct", input: &struct { - Field CustomType `env:"FIELD"` + Field CustomDecoderType `env:"FIELD"` }{}, exp: &struct { - Field CustomType `env:"FIELD"` + Field CustomDecoderType `env:"FIELD"` }{ - Field: CustomType{ + Field: CustomDecoderType{ value: "CUSTOM-foo", }, }, @@ -1112,12 +1258,12 @@ func TestProcessWith(t *testing.T) { { name: "custom_decoder/pointer", input: &struct { - Field *CustomType `env:"FIELD"` + Field *CustomDecoderType `env:"FIELD"` }{}, exp: &struct { - Field *CustomType `env:"FIELD"` + Field *CustomDecoderType `env:"FIELD"` }{ - Field: &CustomType{ + Field: &CustomDecoderType{ value: "CUSTOM-foo", }, }, @@ -1128,7 +1274,7 @@ func TestProcessWith(t *testing.T) { { name: "custom_decoder/private", input: &struct { - field *CustomType `env:"FIELD"` + field *CustomDecoderType `env:"FIELD"` }{}, lookuper: MapLookuper(map[string]string{}), err: ErrPrivateField, @@ -1837,7 +1983,10 @@ func TestProcessWith(t *testing.T) { opts := cmp.AllowUnexported( // Custom decoder type - CustomType{}, + CustomDecoderType{}, + + // Custom standard library interfaces decoder type + CustomStdLibDecodingType{}, // Custom decoder type that returns an error CustomTypeError{},