Skip to content

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 10 commits into from
Jan 12, 2018
Merged

Add support for types which implement encoding.TextUnmarshaler #9

merged 10 commits into from
Jan 12, 2018

Conversation

albrow
Copy link
Contributor

@albrow albrow commented Oct 31, 2017

This allows the go-envvar package to support common types in the standard library such as time.Time. It also allows support for custom types, provided they implement the interface.


// Check if the struct field type implements the encoding.TextUnmarshaler
// interface.
if structField.Type().Implements(reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()) {
Copy link
Contributor

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 when UnmarshalText was called.

Am I missing something @albrow? I think this code path should be an error.

Copy link
Contributor Author

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 the UnmarshalText 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:

// customUnmarshallerWrapper also implements the UnmarshalText method by calling
// it on its own *customUnmarshaller.
type customUnmarshallerWrapper struct {
	um *customUnmarshaller
}

// 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 customUnmarshallerWrapper) UnmarshalText(text []byte) error {
	if cuw.um == nil {
		return nil
	}
	return cuw.um.UnmarshalText(text)
}

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 like map.

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.

Copy link
Contributor

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. 👍

envvar/envvar.go Outdated
results := structField.Addr().MethodByName("UnmarshalText").Call([]reflect.Value{reflect.ValueOf([]byte(v))})
if !results[0].IsNil() {
err := results[0].Interface().(error)
return fmt.Errorf("envvar: Error parsing environment variable %s: %s\n%s", name, v, err.Error())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a test for this new error condition as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌 added.

@albrow
Copy link
Contributor Author

albrow commented Nov 26, 2017

@ntindall thanks for taking a look at this :) I also added some test cases for setting default values on a field which implements TextUnmarshaler.

Copy link
Contributor

@ntindall ntindall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for updating @albrow, looks about good to go. just a few minor comments now.

let me know when its ready for the final pass!

@@ -40,6 +48,9 @@ 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"}},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: inconsistent formatting with line 92

type customUnmarshalerWrapper [3]string

// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment seems out of date now, (i think this is a better example than we have in the discussion, though).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, there were actually some unintended changes included in one of the recent commits. That is why some of the comments don't line up with the code. I agree that the version of customUnmarshalerWrapper which used a slice type ([]string) is easier to understand, but unfortunately it doesn't work. You can't set the value of a slice the way that we would like to.

I've fixed the PR so that now only the intended changes are included, and the comments are re-aligned with the code.

(Technically the built-in slice type contains a pointer to an underlying array. While you can change that array, the changes won't necessarily be reflected in any slices that point to the same array. IMO this idiosyncrasy will lead to more confusion than just using a pointer in our examples and tests).

// an error.
type alwaysErrorUnmarshaler struct{}

func (eu alwaysErrorUnmarshaler) UnmarshalText(text []byte) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe its pedantic, but should we test the case for the pointer receiver as well since that goes down a different code path?


// Check if the struct field type implements the encoding.TextUnmarshaler
// interface.
if structField.Type().Implements(reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()) {
Copy link
Contributor

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. 👍

}

// customUnmarshalerWrapper also implements the UnmarshalText method by calling
// it on its own *customUnmarshaler.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment is out of date now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡️

@albrow
Copy link
Contributor Author

albrow commented Dec 14, 2017

@ntindall I believe I have addressed all of your comments. Should be ready for final review.

Copy link
Contributor

@ntindall ntindall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌱 LGTM

It seems like this is going to conflict with #8.

they both look about good to go, we can include them in the same minor release.

@albrow
Copy link
Contributor Author

albrow commented Dec 16, 2017

Thanks @ntindall. Looks like I don't have the ability to merge, so I'll leave that to you.

@ntindall
Copy link
Contributor

ntindall commented Dec 19, 2017

@albrow could you please rebase this against master and fix the conflicts?

@albrow
Copy link
Contributor Author

albrow commented Jan 9, 2018

@ntindall finished rebasing. My code also uses the new error types now.

@ntindall
Copy link
Contributor

LGTM.

Thanks @albrow !

@ntindall ntindall merged commit 5fd9eb2 into plaid:master Jan 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants