Original post

Hi,

I’m currently using Google’s Github api client repo as a reference for my own api client that I am writing. I’m learning quite a bit from reading their code, but one thing I do not understand is in their github.go source code.

They have a Response struct for http responses from api calls:

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.
        //
        // These fields support what is called "offset pagination" and should
        // be used with the ListOptions struct.
        NextPage  int
        PrevPage  int
        FirstPage int
        LastPage  int

        // Additionally, some APIs support "cursor pagination" instead of offset.
        // This means that a token points directly to the next record which
        // can lead to O(1) performance compared to O(n) performance provided
        // by offset pagination.
        //
        // For APIs that support cursor pagination (such as
        // TeamsService.ListIDPGroupsInOrganization), the following field
        // will be populated to point to the next page.
        //
        // To use this token, set ListCursorOptions.Page to this value before
        // calling the endpoint again.
        NextPageToken string

        // Explicitly specify the Rate type so Rate's String() receiver doesn't
        // propagate to Response.
        Rate Rate
}

Then they implement their own Do function for the client like so:

func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
        if ctx == nil {
                return nil, errors.New("context must be non-nil")
        }
        req = withContext(ctx, req)

        rateLimitCategory := category(req.URL.Path)

        // If we've hit rate limit, don't make further requests before Reset time.
        if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
                return &Response{
                        Response: err.Response,
                        Rate:     err.Rate,
                }, err
        }

        resp, err := c.client.Do(req)
        if err != nil {
                // If we got an error, and the context has been canceled,
                // the context's error is probably more useful.
                select {
                case <-ctx.Done():
                        return nil, ctx.Err()
                default:
                }

                // If the error type is *url.Error, sanitize its URL before returning.
                if e, ok := err.(*url.Error); ok {
                        if url, err := url.Parse(e.URL); err == nil {
                                e.URL = sanitizeURL(url).String()
                                return nil, e
                        }
                }

                return nil, err
        }
        defer resp.Body.Close()

        response := newResponse(resp)

        c.rateMu.Lock()
        c.rateLimits[rateLimitCategory] = response.Rate
        c.rateMu.Unlock()

        err = CheckResponse(resp)
        if err != nil {
                // Special case for AcceptedErrors. If an AcceptedError
                // has been encountered, the response's payload will be
                // added to the AcceptedError and returned.
                //
                // Issue #1022
                aerr, ok := err.(*AcceptedError)
                if ok {
                        b, readErr := ioutil.ReadAll(resp.Body)
                        if readErr != nil {
                                return response, readErr
                        }

                        aerr.Raw = b
                        return response, aerr
                }

                return response, err
        }

        if v != nil {
                if w, ok := v.(io.Writer); ok {
                        io.Copy(w, resp.Body)
                } else {
                        decErr := json.NewDecoder(resp.Body).Decode(v)
                        if decErr == io.EOF {
                                decErr = nil // ignore EOF errors caused by empty response body
                        }
                        if decErr != nil {
                                err = decErr
                        }
                }
        }

        return response, err
}

The last parameter for Do accepts a Github model such as Project{} or Repo{}.

Here is an example from projects.go

// ProjectsService provides access to the projects functions in the
// GitHub API.
//
// GitHub API docs: https://developer.github.com/v3/projects/
type ProjectsService service

// Project represents a GitHub Project.
type Project struct {
        ID         *int64     `json:"id,omitempty"`
        URL        *string    `json:"url,omitempty"`
        HTMLURL    *string    `json:"html_url,omitempty"`
        ColumnsURL *string    `json:"columns_url,omitempty"`
        OwnerURL   *string    `json:"owner_url,omitempty"`
        Name       *string    `json:"name,omitempty"`
        Body       *string    `json:"body,omitempty"`
        Number     *int       `json:"number,omitempty"`
        State      *string    `json:"state,omitempty"`
        CreatedAt  *Timestamp `json:"created_at,omitempty"`
        UpdatedAt  *Timestamp `json:"updated_at,omitempty"`
        NodeID     *string    `json:"node_id,omitempty"`

        // The User object that generated the project.
        Creator *User `json:"creator,omitempty"`
}

func (p Project) String() string {
        return Stringify(p)
}

// GetProject gets a GitHub Project for a repo.
//
// GitHub API docs: https://developer.github.com/v3/projects/#get-a-project
func (s *ProjectsService) GetProject(ctx context.Context, id int64) (*Project, *Response, error) {
        u := fmt.Sprintf("projects/%v", id)
        req, err := s.client.NewRequest("GET", u, nil)
        if err != nil {
                return nil, nil, err
        }

        // TODO: remove custom Accept headers when APIs fully launch.
        req.Header.Set("Accept", mediaTypeProjectsPreview)

        project := &Project{}
        resp, err := s.client.Do(ctx, req, project)
        if err != nil {
                return nil, resp, err
        }

        return project, resp, nil
}

So my question pertains to the Do function. It looks they take an http response, and then wrap it around their own Response{} struct. In the code they take the original response (resp), the Github model (e.g. Project) and json.Decode it.

    if v != nil {
                if w, ok := v.(io.Writer); ok {
                        io.Copy(w, resp.Body)
                } else {
                        decErr := json.NewDecoder(resp.Body).Decode(v)
                        if decErr == io.EOF {
                                decErr = nil // ignore EOF errors caused by empty response body
                        }
                        if decErr != nil {
                                err = decErr
                        }
                }
        }

But if you look at the function they return their own Response{} struct and do nothing with the decoded Github model. So in projects., Project.GetProject would be returning the whole http response rather than the decode Project struct. Is this right? If so, why would you do it like this instead of returning the actual decode project?