-
Notifications
You must be signed in to change notification settings - Fork 4
Add support for types which implement encoding.TextUnmarshaler #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
da3a5f3
Add support for types which implement encoding.TextUnmarshaler
albrow 64c9ce2
Add test case for non-pointer receivers for UnmarshalText
albrow 6a144c0
Add test case for UnmarshalText returning an error
albrow cb7d3da
Add more test cases for default values
albrow b0441fd
Fix some small typos
albrow 263f207
Revert "Fix some small typos"
albrow ac75362
Fix some small typos.
albrow b91333f
Make formatting more consistent in envvar_test.go
albrow 49bd67b
Add additional test case for pointer receivers
albrow ec02555
Use new error types
albrow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,15 @@ | ||
package envvar | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os" | ||
"reflect" | ||
"regexp" | ||
"runtime" | ||
"strings" | ||
"testing" | ||
"time" | ||
) | ||
|
||
func TestParse(t *testing.T) { | ||
|
@@ -25,6 +28,9 @@ func TestParse(t *testing.T) { | |
"FLOAT32": "0.001234", | ||
"FLOAT64": "23.7", | ||
"BOOL": "true", | ||
"TIME": "2017-10-31T14:18:00Z", | ||
"CUSTOM": "foo,bar,baz", | ||
"WRAPPER": "a,b,c", | ||
} | ||
expected := typedVars{ | ||
STRING: "foo", | ||
|
@@ -41,8 +47,24 @@ func TestParse(t *testing.T) { | |
FLOAT32: 0.001234, | ||
FLOAT64: 23.7, | ||
BOOL: true, | ||
TIME: time.Date(2017, 10, 31, 14, 18, 0, 0, time.UTC), | ||
CUSTOM: customUnmarshaler{ | ||
strings: []string{"foo", "bar", "baz"}, | ||
}, | ||
WRAPPER: customUnmarshalerWrapper{ | ||
um: &customUnmarshaler{ | ||
strings: []string{"a", "b", "c"}, | ||
}, | ||
}, | ||
} | ||
// Note that we have to initialize the WRAPPER type so that its field is | ||
// non-nil. No other types need to be initialized. | ||
holder := &typedVars{ | ||
WRAPPER: customUnmarshalerWrapper{ | ||
um: &customUnmarshaler{}, | ||
}, | ||
} | ||
testParse(t, vars, &typedVars{}, expected) | ||
testParse(t, vars, holder, expected) | ||
} | ||
|
||
func TestParseCustomNames(t *testing.T) { | ||
|
@@ -77,8 +99,24 @@ func TestParseDefaultVals(t *testing.T) { | |
FLOAT32: 0.001234, | ||
FLOAT64: 23.7, | ||
BOOL: true, | ||
TIME: time.Date(1992, 9, 29, 0, 0, 0, 0, time.UTC), | ||
CUSTOM: customUnmarshaler{ | ||
strings: []string{"one", "two", "three"}, | ||
}, | ||
WRAPPER: customUnmarshalerWrapper{ | ||
um: &customUnmarshaler{ | ||
strings: []string{"apple", "banana", "cranberry"}, | ||
}, | ||
}, | ||
} | ||
testParse(t, nil, &defaultVars{}, expected) | ||
// Note that we have to initialize the WRAPPER type so that its field is | ||
// non-nil. No other types need to be initialized. | ||
holder := &defaultVars{ | ||
WRAPPER: customUnmarshalerWrapper{ | ||
um: &customUnmarshaler{}, | ||
}, | ||
} | ||
testParse(t, nil, holder, expected) | ||
} | ||
|
||
func TestParseCustomNameAndDefaultVal(t *testing.T) { | ||
|
@@ -121,7 +159,7 @@ func TestParseRequiredVars(t *testing.T) { | |
} | ||
} | ||
|
||
func TestParseWithInvalidArgs(t *testing.T) { | ||
func TestParseErrors(t *testing.T) { | ||
testCases := []struct { | ||
holder interface{} | ||
expectedError string | ||
|
@@ -228,6 +266,77 @@ func expectInvalidVariableError(t *testing.T, err error) { | |
} | ||
} | ||
|
||
func TestUnmarshalTextError(t *testing.T) { | ||
holder := &alwaysErrorVars{} | ||
err := setFieldVal(reflect.ValueOf(holder).Elem().Field(0), "alwaysError", "") | ||
if err == nil { | ||
t.Errorf("Expected InvalidVariableError, but got nil error") | ||
} else if _, ok := err.(InvalidVariableError); !ok { | ||
t.Errorf("Expected InvalidVariableError, but got %s", err.Error()) | ||
} | ||
} | ||
|
||
func TestUnmarshalTextErrorPtr(t *testing.T) { | ||
holder := &alwaysErrorVarsPtr{} | ||
err := setFieldVal(reflect.ValueOf(holder).Elem().Field(0), "alwaysErrorPtr", "") | ||
if err == nil { | ||
t.Errorf("Expected InvalidVariableError, but got nil error") | ||
} else if _, ok := err.(InvalidVariableError); !ok { | ||
t.Errorf("Expected InvalidVariableError, but got %s", err.Error()) | ||
} | ||
} | ||
|
||
// customUnmarshaler implements the UnmarshalText method. | ||
type customUnmarshaler struct { | ||
strings []string | ||
} | ||
|
||
// UnmarshalText simply splits the text by the separator: ",". | ||
func (cu *customUnmarshaler) UnmarshalText(text []byte) error { | ||
cu.strings = strings.Split(string(text), ",") | ||
return nil | ||
} | ||
|
||
// customUnmarshalerWrapper also implements the UnmarshalText method by calling | ||
// it on its own *customUnmarshaler. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comment is out of date now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ⚡️ |
||
type customUnmarshalerWrapper struct { | ||
um *customUnmarshaler | ||
} | ||
|
||
// UnmarshalText simply calls um.UnmarshalText. Note that here we use a | ||
// non-pointer receiver. It still works because the um field is a pointer. We | ||
// just need to be sure to check if um is nil first. | ||
func (cuw customUnmarshalerWrapper) UnmarshalText(text []byte) error { | ||
if cuw.um == nil { | ||
return nil | ||
} | ||
return cuw.um.UnmarshalText(text) | ||
} | ||
|
||
// alwaysErrorUnmarshaler implements the UnmarshalText method by always | ||
// returning an error. | ||
type alwaysErrorUnmarshaler struct{} | ||
|
||
func (aeu alwaysErrorUnmarshaler) UnmarshalText(text []byte) error { | ||
return errors.New("this function always returns an error") | ||
} | ||
|
||
type alwaysErrorVars struct { | ||
AlwaysError alwaysErrorUnmarshaler | ||
} | ||
|
||
// alwaysErrorUnmarshalerPtr is like alwaysErrorUnmarshaler but implements | ||
// the UnmarshalText method with a pointer receiver. | ||
type alwaysErrorUnmarshalerPtr struct{} | ||
|
||
func (aue *alwaysErrorUnmarshalerPtr) UnmarshalText(text []byte) error { | ||
return errors.New("this function always returns an error") | ||
} | ||
|
||
type alwaysErrorVarsPtr struct { | ||
AlwaysErrorPtr alwaysErrorUnmarshalerPtr | ||
} | ||
|
||
type typedVars struct { | ||
STRING string | ||
INT int | ||
|
@@ -243,6 +352,9 @@ type typedVars struct { | |
FLOAT32 float32 | ||
FLOAT64 float64 | ||
BOOL bool | ||
TIME time.Time | ||
CUSTOM customUnmarshaler | ||
WRAPPER customUnmarshalerWrapper | ||
} | ||
|
||
type customNamedVars struct { | ||
|
@@ -253,20 +365,23 @@ type customNamedVars struct { | |
} | ||
|
||
type defaultVars struct { | ||
STRING string `default:"foo"` | ||
INT int `default:"272309480983"` | ||
INT8 int8 `default:"-4"` | ||
INT16 int16 `default:"15893"` | ||
INT32 int32 `default:"-230984"` | ||
INT64 int64 `default:"12"` | ||
UINT uint `default:"42"` | ||
UINT8 uint8 `default:"13"` | ||
UINT16 uint16 `default:"1337"` | ||
UINT32 uint32 `default:"348904"` | ||
UINT64 uint64 `default:"12093803"` | ||
FLOAT32 float32 `default:"0.001234"` | ||
FLOAT64 float64 `default:"23.7"` | ||
BOOL bool `default:"true"` | ||
STRING string `default:"foo"` | ||
INT int `default:"272309480983"` | ||
INT8 int8 `default:"-4"` | ||
INT16 int16 `default:"15893"` | ||
INT32 int32 `default:"-230984"` | ||
INT64 int64 `default:"12"` | ||
UINT uint `default:"42"` | ||
UINT8 uint8 `default:"13"` | ||
UINT16 uint16 `default:"1337"` | ||
UINT32 uint32 `default:"348904"` | ||
UINT64 uint64 `default:"12093803"` | ||
FLOAT32 float32 `default:"0.001234"` | ||
FLOAT64 float64 `default:"23.7"` | ||
BOOL bool `default:"true"` | ||
TIME time.Time `default:"1992-09-29T00:00:00Z"` | ||
CUSTOM customUnmarshaler `default:"one,two,three"` | ||
WRAPPER customUnmarshalerWrapper `default:"apple,banana,cranberry"` | ||
} | ||
|
||
type customNameAndDefaultVars struct { | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe this code path is currently tested. Moreover, I think it should be required that the
TextUnmarshaler
interface be implemented with a pointer receiver, otherwise the holder would not be mutated whenUnmarshalText
was called.Am I missing something @albrow? I think this code path should be an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, it is not impossible for a type to correctly implement
TextUnmarshaler
with a non-pointer receiver for theUnmarshalText
method. If you have a reference type within the receiver type, it will be settable even if the receiver itself is not. I added a test case to illustrate this:It doesn't have to a pointer, either. It could also be a type whose underlying type is a pointer (e.g.
type myCustomType *int
) or a builtin reference type likemap
.Honestly though, after looking at this again, I'm not sure how often it will occur in the real world. I would be open to hearing an argument that this edge case is not necessary and should be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I hadn't considered this...
While the example you have here with
customUnmarshallerWrapper
seems a little unlikely to come up, I think think unmarshalling to a builtin reference type is something we would want to support. 👍