From 18ed217bf582c071fabd751564326c0f00199b23 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Mon, 29 Jul 2013 16:20:37 -0700 Subject: [PATCH] add new Response object which wraps http.Response This allows use to provide convenience methods for accessing the paginations links that are returned in HTTP Link headers. To expose that, we'll need to return the Response from all API methods, which will also allow users of the client to do any other kind of inspection of the response. This adds additional complexity to the API and will be a breaking change, but seems to be the cleanest way to enable this sort of thing. Refs #22 --- github/github.go | 110 ++++++++++++++++++++++++++++++++++++------ github/github_test.go | 53 ++++++++++++++++++++ 2 files changed, 148 insertions(+), 15 deletions(-) diff --git a/github/github.go b/github/github.go index 5275f19..f6ae75e 100644 --- a/github/github.go +++ b/github/github.go @@ -53,6 +53,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" ) @@ -156,10 +157,96 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ return req, nil } +// Response is a GitHub API response. This wraps the standard http.Response +// returned from GitHub and provides convenient access to things like +// pagination links. +type Response struct { + *http.Response + + // These fields provide the page values for paginating through a set of + // results. Any or all of these may be set to the zero value for + // responses that are not part of a paginated set, or for which there + // are no additional pages. + + NextPage int + PrevPage int + FirstPage int + LastPage int + + Rate +} + +// newResponse creats a new Response for the provided http.Response. +func newResponse(r *http.Response) *Response { + response := &Response{Response: r} + response.populatePageValues() + response.populateRate() + return response +} + +// populatePageValues parses the HTTP Link response headers and populates the +// various pagination link values in the Reponse. +func (r *Response) populatePageValues() { + if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 { + for _, link := range strings.Split(links[0], ",") { + segments := strings.Split(link, ";") + + // link must at least have href and rel + if len(segments) < 2 { + continue + } + + // ensure href is properly formatted + if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") { + continue + } + + // try to pull out page parameter + url, err := url.Parse(segments[0][1 : len(segments[0])-1]) + if err != nil { + continue + } + page := url.Query().Get("page") + if page == "" { + continue + } + + for _, segment := range segments[1:] { + switch strings.TrimSpace(segment) { + case `rel="next"`: + r.NextPage, _ = strconv.Atoi(page) + case `rel="prev"`: + r.PrevPage, _ = strconv.Atoi(page) + case `rel="first"`: + r.FirstPage, _ = strconv.Atoi(page) + case `rel="last"`: + r.LastPage, _ = strconv.Atoi(page) + } + + } + } + } +} + +// populateRate parses the rate related headers and populates the response Rate. +func (r *Response) populateRate() { + if limit := r.Header.Get(headerRateLimit); limit != "" { + r.Rate.Limit, _ = strconv.Atoi(limit) + } + if remaining := r.Header.Get(headerRateRemaining); remaining != "" { + r.Rate.Remaining, _ = strconv.Atoi(remaining) + } + if reset := r.Header.Get(headerRateReset); reset != "" { + if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { + r.Rate.Reset = time.Unix(v, 0) + } + } +} + // Do sends an API request and returns the API response. The API response is // decoded and stored in the value pointed to by v, or returned as an error if // an API error has occurred. -func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { +func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { resp, err := c.client.Do(req) if err != nil { return nil, err @@ -167,28 +254,21 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) { defer resp.Body.Close() - // update rate limit - if limit := resp.Header.Get(headerRateLimit); limit != "" { - c.Rate.Limit, _ = strconv.Atoi(limit) - } - if remaining := resp.Header.Get(headerRateRemaining); remaining != "" { - c.Rate.Remaining, _ = strconv.Atoi(remaining) - } - if reset := resp.Header.Get(headerRateReset); reset != "" { - if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { - c.Rate.Reset = time.Unix(v, 0) - } - } + response := newResponse(resp) + + c.Rate = response.Rate err = CheckResponse(resp) if err != nil { - return resp, err + // even though there was an error, we still return the response + // in case the caller wants to inspect it further + return response, err } if v != nil { err = json.NewDecoder(resp.Body).Decode(v) } - return resp, err + return response, err } /* diff --git a/github/github_test.go b/github/github_test.go index 5cb3fe1..c382ae3 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -130,6 +130,59 @@ func TestNewRequest_badURL(t *testing.T) { testURLParseError(t, err) } +func TestResponse_populatePageValues(t *testing.T) { + r := http.Response{ + Header: http.Header{ + "Link": {`; rel="first",` + + `; rel="prev",` + + `; rel="next",` + + `; rel="last"`, + }, + }, + } + + response := newResponse(&r) + if want, got := 1, response.FirstPage; want != got { + t.Errorf("response.FirstPage: %v, want %v", want, got) + } + if want, got := 2, response.PrevPage; want != got { + t.Errorf("response.PrevPage: %v, want %v", want, got) + } + if want, got := 4, response.NextPage; want != got { + t.Errorf("response.NextPage: %v, want %v", want, got) + } + if want, got := 5, response.LastPage; want != got { + t.Errorf("response.LastPage: %v, want %v", want, got) + } +} + +func TestResponse_populatePageValues_invalid(t *testing.T) { + r := http.Response{ + Header: http.Header{ + "Link": {`,` + + `; rel="first",` + + `https://api.github.com/?page=2; rel="prev",` + + `; rel="next",` + + `; rel="last"`, + }, + }, + } + + response := newResponse(&r) + if want, got := 0, response.FirstPage; want != got { + t.Errorf("response.FirstPage: %v, want %v", want, got) + } + if want, got := 0, response.PrevPage; want != got { + t.Errorf("response.PrevPage: %v, want %v", want, got) + } + if want, got := 0, response.NextPage; want != got { + t.Errorf("response.NextPage: %v, want %v", want, got) + } + if want, got := 0, response.LastPage; want != got { + t.Errorf("response.LastPage: %v, want %v", want, got) + } +} + func TestDo(t *testing.T) { setup() defer teardown()