Skip to content

Commit

Permalink
Change custom unmarshaling order (#59)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gust1n committed Jun 9, 2022
1 parent befaf9a commit 39c9bc7
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 21 deletions.
16 changes: 8 additions & 8 deletions envconfig.go
Expand Up @@ -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
}
}
Expand Down
175 changes: 162 additions & 13 deletions envconfig_test.go
Expand Up @@ -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)
Expand Down Expand Up @@ -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",
},
},
Expand All @@ -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",
},
},
Expand All @@ -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,
Expand Down Expand Up @@ -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{},
Expand Down

0 comments on commit 39c9bc7

Please sign in to comment.