Skip to content
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

Add context support, ErrUnauthorized and Doer interface for http.Client #4

Merged
merged 13 commits into from
Oct 15, 2016

Conversation

anpryl
Copy link

@anpryl anpryl commented Sep 19, 2016

Extend API for context usage.
Make Errors an error instance.

@coveralls
Copy link

coveralls commented Sep 19, 2016

Coverage Status

Coverage decreased (-7.6%) to 59.603% when pulling 0cc56ad on anpryl:master into 73e6538 on tambet:master.

Copy link
Collaborator

@dmitshur dmitshur left a comment

Choose a reason for hiding this comment

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

Thank you for contributing.

Aside from the fmt.Println(resp.StatusCode, string(b)) change, which looks unintended, all other code changes look good.

However, I see two main issues/potential improvements for this PR.

  1. All code changes are in a single commit. This is harder to review, and not a good practice. Would you mind splitting each logical change like "add context support, "add Doer interface", "add http post form", etc., into a separate commit?
  2. No documentation. I know a lot of the code here isn't perfect, but let's try to make things better. Exported symbols need to be documented. This is a good practice and consistent with Go style. Try running golint on your changes.

Aside from those 2 things, these changes look great and make sense to me. I'd be happy to accept them. I'm guessing @tambet would like them too.

Are you willing to address the two suggestions I've made above?

fmt.Println("---------------------------")
fmt.Println(resp.StatusCode, string(b))
fmt.Println("---------------------------")
err = json.NewDecoder(bytes.NewBuffer(b)).Decode(res)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you forget to remove this change after testing?

Copy link
Author

Choose a reason for hiding this comment

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

I'm so sorry for this debugging printing
I will add documentation and split commits at this weekend.
Thank you for your feedback!

@coveralls
Copy link

Coverage Status

Coverage decreased (-0.09%) to 67.105% when pulling 98380c1 on anpryl:master into 73e6538 on tambet:master.

@coveralls
Copy link

coveralls commented Sep 20, 2016

Coverage Status

Coverage decreased (-0.09%) to 67.105% when pulling 98380c1 on anpryl:master into 73e6538 on tambet:master.

@anpryl
Copy link
Author

anpryl commented Sep 20, 2016

Tried to split commits as much as possible, add documentation, also add tests this time.
If I could make it better, please, don't hesitate to comment it.
Thank you!

@coveralls
Copy link

coveralls commented Sep 20, 2016

Coverage Status

Coverage decreased (-0.09%) to 67.105% when pulling 12d49ff on anpryl:master into 73e6538 on tambet:master.

Copy link
Collaborator

@dmitshur dmitshur left a comment

Choose a reason for hiding this comment

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

This looks great. I've left minor comments and questions in my review. Thanks!


//DoerFunc implements Doer interface.
//Allow to transform any appropriate function "f" to Doer instance: DoerFunc(f).
DoerFunc func(req *http.Request) (resp *http.Response, err error)
Copy link
Collaborator

@dmitshur dmitshur Sep 23, 2016

Choose a reason for hiding this comment

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

Here, and elsewhere, the comments don't have a space after the two slashes. There should be a single space there.

See here for rationale.

Copy link
Author

Choose a reason for hiding this comment

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

Fix this, thank you for explanations.

//Use it as point of setting Auth header or custom status code error handling.
Doer interface {
Do(req *http.Request) (*http.Response, error)
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I have a guess, but could you please elaborate on why you decided to make the Doer interface instead of sticking with an *http.Client?

Copy link
Author

Choose a reason for hiding this comment

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

It allows me to use single instance of my configured http.Client and set different Auth headers on each request.

I have configured http.Client and it is property of my own Client struct.

type Client struct {
    httpClient *http.Client
}

My service receiving auth token as an argument to request and with Doer interface it is easy to add Auth header with token to request like this:

func (c *Client) withToken(token string) *asana.Client {
    f := func(req *http.Request) (*http.Response, error) {
        req.Header.Set("Authorization", "Bearer "+token)
        return c.httpClient.Do(req)
    }
    return asana.NewClient(asana.DoerFunc(f))
}

// Usage example
func (c *Client) Issue(ctx context.Context, token string, issueID int64) (*Issue, error) {
    t, err := c.withToken(token).GetTask(ctx, issueID, issueFieldsFilter)
    if err != nil {
        return nil, err
    }
    issue := toIssue(t)
    return &issue, nil
}

Copy link
Collaborator

@dmitshur dmitshur Sep 30, 2016

Choose a reason for hiding this comment

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

Thanks for the explanation.

This sounds reasonable, but I also slightly worry this might not be the most optimal solution for the problem you're trying to solve. Having Doer be an interface lets you pass in a middleware-augmented *http.Client of sorts, but you still have to create a new asana.NewClient for each one...

It's also interesting that go-github, a very popular library, manages to get by without Doer interface. /cc @willnorris, do you know how one would do that with go-github?

I'm not opposed to this going in @anpryl, I just wanted to say that it seems a little more questionable to me, and perhaps we can find a better way to effectively be able to augment certain *http.Requests. But maybe not, and this is fine.

Copy link

@willnorris willnorris Sep 30, 2016

Choose a reason for hiding this comment

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

Is this client fully taking on responsibility for authentication, or only for certain calls, like in your c.Issue method? go-github's Client knows nothing about authentication at all. It is always punted to the underlying http.Client, which typically comes from golang.org/x/oauth2, or the basic auth client that go-github does provide (but is otherwise completely separated from the Client struct).

I think the best analogy to this in go-github would be the custom accept headers we set on various API calls. That happens by first building the http.Request, then adding whatever custom headers we want before passing it off to client.Do. As long as the underlying http.Client leaves those headers intact, then you're fine. If your underlying http.Client is clobbering your auth header because it thinks it should be responsible for auth, then you do have a problem.

This is exactly why go-github's Client struct has its own Do and NewRequest methods, so that you can modify the request before calling Do. Not separating those, as go-asana does with its single Request method, results in these weird workarounds.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the explanation @willnorris, that makes sense.

I agree that the Do and NewRequest separation makes more sense and is cleaner/safer, but I'd want to think more and understand it better (outside of this PR). I think we should consider changing this library in that direction, rather than the Doer, which I think is a lot harder for people to use (at all, and also correctly).

What are your thoughts on this @anpryl?

Copy link
Author

Choose a reason for hiding this comment

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

We still need Doer to reuse http.Client with different tokens.
But this change will decrease cost of NewClient function call.
We could also include withToken function into package and make it public.

Copy link
Collaborator

@dmitshur dmitshur Oct 14, 2016

Choose a reason for hiding this comment

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

Well, okay. It's been a while and I have no better ideas than Doer interface, so let's go with that for now. If really good ideas come in the future, we can probably still change things.

Copy link
Author

Choose a reason for hiding this comment

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

Can I move url.URL to global variable and initialize it in init() funciton?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's better to split up orthogonal changes across multiple PRs and keep each one smaller.

Mind making it a separate followup PR? This one is already huge. :)

Copy link
Author

Choose a reason for hiding this comment

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

Well, you are right, I will prepare new PR after merge.

}

Error struct {
Phrase string `json:"phrase,omitempty"`
Message string `json:"message,omitempty"`
}

Errors []Error
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are there any restrictions on Errors? Can it contain 0 elements? 1 element?

@@ -272,7 +283,7 @@ func (c *Client) request(method string, path string, data interface{}, opt *Filt
res := &Response{Data: v}
err = json.NewDecoder(resp.Body).Decode(res)
if len(res.Errors) > 0 {
return res.Errors[0]
return res.Errors
Copy link
Collaborator

Choose a reason for hiding this comment

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

According to this, at least 1 element. I think it's worth documenting that for the Errors type, so users will know to always expect either at least one error there.

Copy link
Author

Choose a reason for hiding this comment

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

You are correct, add godoc for Errors type

@@ -14,6 +15,7 @@ var (
client *Client
mux *http.ServeMux
server *httptest.Server
ctx = context.Background()
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know it's a test, but still, this isn't saving much typing. Instead of this variable here, just use context.Background() directly at the call sites. E.g.:

workspaces, err := client.ListWorkspaces(context.Background())
...
users, err := client.ListUsers(context.Background(), nil)
...

Copy link
Author

Choose a reason for hiding this comment

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

I prefer to write 3 letters instead of context.Background() in tests, but it's not a big deal. I remove this global variable and put context.Background().

}

// request makes a request to Asana API, using method, at path, sending data with opt filter.
// The response is populated into v, and any error is returned.
func (c *Client) request(ctx context.Context, method string, path string, data interface{}, opt *Filter, v interface{}) error {
func (c *Client) request(ctx context.Context, method string, path string, data interface{}, form url.Values, opt *Filter, v interface{}) error {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Now request has 2 ways of specifying the data. Either by setting data, or by setting form. I think it might be helpful to document that at most one can be used at the same time (or none), and that data takes higher precedence.

What do you think?

Copy link
Author

Choose a reason for hiding this comment

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

Improve request documentation

@coveralls
Copy link

coveralls commented Sep 25, 2016

Coverage Status

Coverage decreased (-0.09%) to 67.105% when pulling 67b650e on anpryl:master into 73e6538 on tambet:master.

@coveralls
Copy link

coveralls commented Sep 25, 2016

Coverage Status

Coverage decreased (-0.09%) to 67.105% when pulling 84ac3b0 on anpryl:master into 73e6538 on tambet:master.

Copy link
Collaborator

@dmitshur dmitshur 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 addressing all my feedback @anpryl.

This LGTM.

@tambet, any comments on this? If this looks good to you, or if you don't have any objections, I'll merge this in a few days.

//Use it as point of setting Auth header or custom status code error handling.
Doer interface {
Do(req *http.Request) (*http.Response, error)
}
Copy link
Collaborator

@dmitshur dmitshur Sep 30, 2016

Choose a reason for hiding this comment

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

Thanks for the explanation.

This sounds reasonable, but I also slightly worry this might not be the most optimal solution for the problem you're trying to solve. Having Doer be an interface lets you pass in a middleware-augmented *http.Client of sorts, but you still have to create a new asana.NewClient for each one...

It's also interesting that go-github, a very popular library, manages to get by without Doer interface. /cc @willnorris, do you know how one would do that with go-github?

I'm not opposed to this going in @anpryl, I just wanted to say that it seems a little more questionable to me, and perhaps we can find a better way to effectively be able to augment certain *http.Requests. But maybe not, and this is fine.

This is more consistent with the rest of the code.
@coveralls
Copy link

coveralls commented Sep 30, 2016

Coverage Status

Coverage decreased (-0.09%) to 67.105% when pulling 84152d5 on anpryl:master into 73e6538 on tambet:master.

Copy link
Collaborator

@dmitshur dmitshur left a comment

Choose a reason for hiding this comment

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

LGTM. See #4 (comment).

I'll let this sit for a day and merge tomorrow if no further comments. /cc @tambet

Thanks for your work @anpryl!

@dmitshur dmitshur merged commit 6c0cb70 into tambet:master Oct 15, 2016
@dmitshur
Copy link
Collaborator

Merged. This is a breaking API change, but it includes some necessary API improvements such as adding support for passing a context. Updating should be easy, and those who don't want to will have this library vendored anyway.

Thanks for your awesome contribution @anpryl!

dmitshur added a commit to shurcooL/users that referenced this pull request Oct 15, 2016
dmitshur added a commit to shurcooL/issues that referenced this pull request Oct 15, 2016
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.

4 participants