Skip to content

Commit dfd2ad3

Browse files
hypnoglowteepark
authored andcommitted
Add support for encoding.BinaryUnmarshaler (kelseyhightower#101)
Fixes kelseyhightower#98
1 parent 462fda1 commit dfd2ad3

File tree

9 files changed

+160
-9
lines changed

9 files changed

+160
-9
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ envconfig supports supports these struct field types:
159159
* slices of any supported type
160160
* maps (keys and values of any supported type)
161161
* [encoding.TextUnmarshaler](https://golang.org/pkg/encoding/#TextUnmarshaler)
162+
* [encoding.BinaryUnmarshaler](https://golang.org/pkg/encoding/#BinaryUnmarshaler)
162163

163164
Embedded structs using these fields are also supported.
164165

envconfig.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func gatherInfo(prefix string, spec interface{}) ([]varInfo, error) {
122122

123123
if f.Kind() == reflect.Struct {
124124
// honor Decode if present
125-
if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil {
125+
if decoderFrom(f) == nil && setterFrom(f) == nil && textUnmarshaler(f) == nil && binaryUnmarshaler(f) == nil {
126126
innerPrefix := prefix
127127
if !ftype.Anonymous {
128128
innerPrefix = info.Key
@@ -209,6 +209,10 @@ func processField(value string, field reflect.Value) error {
209209
return t.UnmarshalText([]byte(value))
210210
}
211211

212+
if b := binaryUnmarshaler(field); b != nil {
213+
return b.UnmarshalBinary([]byte(value))
214+
}
215+
212216
if typ.Kind() == reflect.Ptr {
213217
typ = typ.Elem()
214218
if field.IsNil() {
@@ -320,6 +324,11 @@ func textUnmarshaler(field reflect.Value) (t encoding.TextUnmarshaler) {
320324
return t
321325
}
322326

327+
func binaryUnmarshaler(field reflect.Value) (b encoding.BinaryUnmarshaler) {
328+
interfaceFrom(field, func(v interface{}, ok *bool) { b, *ok = v.(encoding.BinaryUnmarshaler) })
329+
return b
330+
}
331+
323332
func isTrue(s string) bool {
324333
b, _ := strconv.ParseBool(s)
325334
return b

envconfig_1.8_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// +build go1.8
2+
3+
package envconfig
4+
5+
import (
6+
"os"
7+
"net/url"
8+
"testing"
9+
"errors"
10+
)
11+
12+
type SpecWithURL struct {
13+
UrlValue url.URL
14+
UrlPointer *url.URL
15+
}
16+
17+
func TestParseURL(t *testing.T) {
18+
var s SpecWithURL
19+
20+
os.Clearenv()
21+
os.Setenv("ENV_CONFIG_URLVALUE", "https://github.com/kelseyhightower/envconfig")
22+
os.Setenv("ENV_CONFIG_URLPOINTER", "https://github.com/kelseyhightower/envconfig")
23+
24+
err := Process("env_config", &s)
25+
if err != nil {
26+
t.Fatal("unexpected error:", err)
27+
}
28+
29+
u, err := url.Parse("https://github.com/kelseyhightower/envconfig")
30+
if err != nil {
31+
t.Fatalf("unexpected error: %v", err)
32+
}
33+
34+
if s.UrlValue != *u {
35+
t.Errorf("expected %q, got %q", u, s.UrlValue.String())
36+
}
37+
38+
if *s.UrlPointer != *u {
39+
t.Errorf("expected %q, got %q", u, s.UrlPointer)
40+
}
41+
}
42+
43+
func TestParseURLError(t *testing.T) {
44+
var s SpecWithURL
45+
46+
os.Clearenv()
47+
os.Setenv("ENV_CONFIG_URLPOINTER", "http_://foo")
48+
49+
err := Process("env_config", &s)
50+
51+
v, ok := err.(*ParseError)
52+
if !ok {
53+
t.Fatalf("expected ParseError, got %T %v", err, err)
54+
}
55+
if v.FieldName != "UrlPointer" {
56+
t.Errorf("expected %s, got %v", "UrlPointer", v.FieldName)
57+
}
58+
59+
expectedUnerlyingError := url.Error{
60+
Op: "parse",
61+
URL: "http_://foo",
62+
Err: errors.New("first path segment in URL cannot contain colon"),
63+
}
64+
65+
if v.Err.Error() != expectedUnerlyingError.Error() {
66+
t.Errorf("expected %q, got %q", expectedUnerlyingError, v.Err)
67+
}
68+
}

envconfig_test.go

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"os"
1111
"testing"
1212
"time"
13+
"net/url"
1314
)
1415

1516
type HonorDecodeInStruct struct {
@@ -21,6 +22,16 @@ func (h *HonorDecodeInStruct) Decode(env string) error {
2122
return nil
2223
}
2324

25+
type CustomURL struct {
26+
Value *url.URL
27+
}
28+
29+
func (cu *CustomURL) UnmarshalBinary(data []byte) error {
30+
u, err := url.Parse(string(data))
31+
cu.Value = u
32+
return err
33+
}
34+
2435
type Specification struct {
2536
Embedded `desc:"can we document a struct"`
2637
EmbeddedButIgnored `ignored:"true"`
@@ -53,6 +64,8 @@ type Specification struct {
5364
DecodeStruct HonorDecodeInStruct `envconfig:"honor"`
5465
Datetime time.Time
5566
MapField map[string]string `default:"one:two,three:four"`
67+
UrlValue CustomURL
68+
UrlPointer *CustomURL
5669
}
5770

5871
type Embedded struct {
@@ -89,6 +102,8 @@ func TestProcess(t *testing.T) {
89102
os.Setenv("ENV_CONFIG_HONOR", "honor")
90103
os.Setenv("ENV_CONFIG_DATETIME", "2016-08-16T18:57:05Z")
91104
os.Setenv("ENV_CONFIG_MULTI_WORD_VAR_WITH_AUTO_SPLIT", "24")
105+
os.Setenv("ENV_CONFIG_URLVALUE", "https://github.com/kelseyhightower/envconfig")
106+
os.Setenv("ENV_CONFIG_URLPOINTER", "https://github.com/kelseyhightower/envconfig")
92107
err := Process("env_config", &s)
93108
if err != nil {
94109
t.Error(err.Error())
@@ -171,6 +186,19 @@ func TestProcess(t *testing.T) {
171186
if s.MultiWordVarWithAutoSplit != 24 {
172187
t.Errorf("expected %q, got %q", 24, s.MultiWordVarWithAutoSplit)
173188
}
189+
190+
u, err := url.Parse("https://github.com/kelseyhightower/envconfig")
191+
if err != nil {
192+
t.Fatalf("unexpected error: %v", err)
193+
}
194+
195+
if *s.UrlValue.Value != *u {
196+
t.Errorf("expected %q, got %q", u, s.UrlValue.Value.String())
197+
}
198+
199+
if *s.UrlPointer.Value != *u {
200+
t.Errorf("expected %q, got %q", u, s.UrlPointer.Value.String())
201+
}
174202
}
175203

176204
func TestParseErrorBool(t *testing.T) {
@@ -669,7 +697,7 @@ func TestTextUnmarshalerError(t *testing.T) {
669697
t.Errorf("expected ParseError, got %v", v)
670698
}
671699
if v.FieldName != "Datetime" {
672-
t.Errorf("expected %s, got %v", "Debug", v.FieldName)
700+
t.Errorf("expected %s, got %v", "Datetime", v.FieldName)
673701
}
674702

675703
expectedLowLevelError := time.ParseError{
@@ -682,8 +710,34 @@ func TestTextUnmarshalerError(t *testing.T) {
682710
if v.Err.Error() != expectedLowLevelError.Error() {
683711
t.Errorf("expected %s, got %s", expectedLowLevelError, v.Err)
684712
}
685-
if s.Debug != false {
686-
t.Errorf("expected %v, got %v", false, s.Debug)
713+
}
714+
715+
func TestBinaryUnmarshalerError(t *testing.T) {
716+
var s Specification
717+
os.Clearenv()
718+
os.Setenv("ENV_CONFIG_REQUIREDVAR", "foo")
719+
os.Setenv("ENV_CONFIG_URLPOINTER", "http://%41:8080/")
720+
721+
err := Process("env_config", &s)
722+
723+
v, ok := err.(*ParseError)
724+
if !ok {
725+
t.Fatalf("expected ParseError, got %T %v", err, err)
726+
}
727+
if v.FieldName != "UrlPointer" {
728+
t.Errorf("expected %s, got %v", "UrlPointer", v.FieldName)
729+
}
730+
731+
// To be compatible with go 1.5 and lower we should do a very basic check,
732+
// because underlying error message varies in go 1.5 and go 1.6+.
733+
734+
ue, ok := v.Err.(*url.Error)
735+
if !ok {
736+
t.Errorf("expected error type to be \"*url.Error\", got %T", v.Err)
737+
}
738+
739+
if ue.Op != "parse" {
740+
t.Errorf("expected error op to be \"parse\", got %q", ue.Op)
687741
}
688742
}
689743

testdata/custom.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ ENV_CONFIG_AFTERNESTED=
2929
ENV_CONFIG_HONOR=
3030
ENV_CONFIG_DATETIME=
3131
ENV_CONFIG_MAPFIELD=
32+
ENV_CONFIG_URLVALUE=
33+
ENV_CONFIG_URLPOINTER=

testdata/default_list.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,13 @@ ENV_CONFIG_MAPFIELD
156156
..[type]........Comma-separated.list.of.String:String.pairs
157157
..[default].....one:two,three:four
158158
..[required]....
159+
ENV_CONFIG_URLVALUE
160+
..[description].
161+
..[type]........CustomURL
162+
..[default].....
163+
..[required]....
164+
ENV_CONFIG_URLPOINTER
165+
..[description].
166+
..[type]........CustomURL
167+
..[default].....
168+
..[required]....

testdata/default_table.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ ENV_CONFIG_AFTERNESTED...........................String.........................
3333
ENV_CONFIG_HONOR.................................HonorDecodeInStruct...............................................................
3434
ENV_CONFIG_DATETIME..............................Time..............................................................................
3535
ENV_CONFIG_MAPFIELD..............................Comma-separated.list.of.String:String.pairs.....one:two,three:four................
36+
ENV_CONFIG_URLVALUE..............................CustomURL.........................................................................
37+
ENV_CONFIG_URLPOINTER............................CustomURL.........................................................................

testdata/fault.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@
2929
{.Key}
3030
{.Key}
3131
{.Key}
32+
{.Key}
33+
{.Key}

usage.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,21 @@ KEY TYPE DEFAULT REQUIRED DESCRIPTION
3737
)
3838

3939
var (
40-
decoderType = reflect.TypeOf((*Decoder)(nil)).Elem()
41-
setterType = reflect.TypeOf((*Setter)(nil)).Elem()
42-
unmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
40+
decoderType = reflect.TypeOf((*Decoder)(nil)).Elem()
41+
setterType = reflect.TypeOf((*Setter)(nil)).Elem()
42+
textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
43+
binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()
4344
)
4445

4546
func implementsInterface(t reflect.Type) bool {
4647
return t.Implements(decoderType) ||
4748
reflect.PtrTo(t).Implements(decoderType) ||
4849
t.Implements(setterType) ||
4950
reflect.PtrTo(t).Implements(setterType) ||
50-
t.Implements(unmarshalerType) ||
51-
reflect.PtrTo(t).Implements(unmarshalerType)
51+
t.Implements(textUnmarshalerType) ||
52+
reflect.PtrTo(t).Implements(textUnmarshalerType) ||
53+
t.Implements(binaryUnmarshalerType) ||
54+
reflect.PtrTo(t).Implements(binaryUnmarshalerType)
5255
}
5356

5457
// toTypeDescription converts Go types into a human readable description

0 commit comments

Comments
 (0)