From b614e4217271ab37c3d191c607a48cb3f027a29b Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Sun, 17 May 2015 20:36:19 -0700 Subject: [PATCH 01/11] Change Receive and Do to support JSON decoding error structs * Start considering 2XX and non-2XX responses separately for decoding * Make Receive and Do accept successV and failureV interface pointers * Add ReceiveSuccess which accepts successV for decoding 2XX responses --- README.md | 49 ++++++++++++++++++++++++------------- examples/github.go | 39 +++++++++++++++++++++++++----- sling.go | 60 +++++++++++++++++++++++++++++++++------------- sling_test.go | 4 ++-- 4 files changed, 111 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 2f060cc..781610b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Sling is a Go REST client library for creating and sending API requests. -Slings store http Request properties to simplify sending requests and decoding responses. Check [usage](#usage) or the [examples](examples) to learn how to compose a Sling into your API client. +Slings store HTTP Request properties to simplify sending requests and decoding responses. Check [usage](#usage) or the [examples](examples) to learn how to compose a Sling into your API client. ### Features @@ -12,8 +12,8 @@ Slings store http Request properties to simplify sending requests and decoding r * Method Setters: Get/Post/Put/Patch/Delete/Head * Add and Set Request Headers * Encode url structs into URL query parameters -* Encode url structs or json into the Request Body -* Decode received JSON success responses +* Encode json or a form into the Request Body +* Receive JSON success or failure responses ## Install @@ -42,13 +42,13 @@ users := base.New().Path("users/") statuses := base.New().Path("statuses/") ``` -Choose an http method, set query parameters, and send the request. +Choose an HTTP method, set query parameters, and send the request. ```go statuses.New().Get("show.json").QueryStruct(params).Receive(tweet) ``` -The sections below provide more details about setting headers, query parameters, body data, and decoding a typed response after sending. +The sections below provide more details about setting headers, query parameters, body data, and decoding a typed response. ### Headers @@ -133,15 +133,11 @@ Requests will include an `application/x-www-form-urlencoded` Content-Type header ### Receive -Define expected value structs. Use `Receive(v interface{})` to send a new Request that will automatically decode the response into the value. +Define a JSON struct to decode a type from 2XX success responses. Use `ReceiveSuccess(successV interface{})` to send a new Request and decode the response body into `successV` if it succeeds. ```go // Github Issue (abbreviated) type Issue struct { - Id int `json:"id"` - Url string `json:"url"` - Number int `json:"number"` - State string `json:"state"` Title string `json:"title"` Body string `json:"body"` } @@ -149,10 +145,33 @@ type Issue struct { ```go issues := new([]Issue) -resp, err := githubBase.New().Get(path).QueryStruct(params).Receive(issues) +resp, err := githubBase.New().Get(path).QueryStruct(params).ReceiveSuccess(issues) fmt.Println(issues, resp, err) ``` +Most APIs return failure responses with JSON error details. To decode these, define success and failure JSON structs. Use `Receive(successV, failureV interface{})` to send a new Request that will automatically decode the response into the `successV` for 2XX responses or into `failureV` for non-2XX responses. + +```go +type GithubError struct { + Message string `json:"message"` + Errors []struct { + Resource string `json:"resource"` + Field string `json:"field"` + Code string `json:"code"` + } `json:"errors"` + DocumentationURL string `json:"documentation_url"` +} +``` + +```go +issues := new([]Issue) +githubError := new(GithubError) +resp, err := githubBase.New().Get(path).QueryStruct(params).Receive(issues, githubError) +fmt.Println(issues, githubError, resp, err) +``` + +Pass a nil `successV` or `failureV` argument to skip JSON decoding into that value. + ### Build an API APIs typically define an endpoint (also called a service) for each type of resource. For example, here is a tiny Github IssueService which [creates](https://developer.github.com/v3/issues/#create-an-issue) and [lists](https://developer.github.com/v3/issues/#list-issues-for-a-repository) repository issues. @@ -171,14 +190,14 @@ func NewIssueService(httpClient *http.Client) *IssueService { func (s *IssueService) Create(owner, repo string, issueBody *IssueRequest) (*Issue, *http.Response, error) { issue := new(Issue) path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) - resp, err := s.sling.New().Post(path).JsonBody(issueBody).Receive(issue) + resp, err := s.sling.New().Post(path).JsonBody(issueBody).ReceiveSuccess(issue) return issue, resp, err } func (srvc IssueService) List(owner, repo string, params *IssueParams) ([]Issue, *http.Response, error) { var issues []Issue path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) - resp, err := srvc.sling.New().Get(path).QueryStruct(params).Receive(&issues) + resp, err := srvc.sling.New().Get(path).QueryStruct(params).ReceiveSuccess(&issues) return *issues, resp, err } ``` @@ -189,10 +208,6 @@ func (srvc IssueService) List(owner, repo string, params *IssueParams) ([]Issue, Create a Pull Request to add a link to your own API. -## Roadmap - -* Receive custom error structs - ## Motivation Many client libraries follow the lead of [google/go-github](https://github.com/google/go-github) (our inspiration!), but do so by reimplementing logic common to all clients. diff --git a/examples/github.go b/examples/github.go index 1e68c0c..1622bcc 100644 --- a/examples/github.go +++ b/examples/github.go @@ -40,6 +40,21 @@ type IssueListParams struct { Since string `url:"since,omitempty"` } +// https://developer.github.com/v3/#client-errors +type GithubError struct { + Message string `json:"message"` + Errors []struct { + Resource string `json:"resource"` + Field string `json:"field"` + Code string `json:"code"` + } `json:"errors"` + DocumentationURL string `json:"documentation_url"` +} + +func (e GithubError) Error() string { + return fmt.Sprintf("github: %v %+v %v", e.Message, e.Errors, e.DocumentationURL) +} + // Implement services type IssueService struct { @@ -54,22 +69,34 @@ func NewIssueService(httpClient *http.Client) *IssueService { func (s *IssueService) List(params *IssueListParams) ([]Issue, *http.Response, error) { issues := new([]Issue) - resp, err := s.sling.New().Path("issues").QueryStruct(params).Receive(issues) - return *issues, resp, err + githubError := new(GithubError) + resp, err := s.sling.New().Path("issues").QueryStruct(params).Receive(issues, githubError) + if err != nil { + return *issues, resp, err + } + return *issues, resp, githubError } func (s *IssueService) ListByRepo(owner, repo string, params *IssueListParams) ([]Issue, *http.Response, error) { issues := new([]Issue) + githubError := new(GithubError) path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) - resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(issues) - return *issues, resp, err + resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(issues, githubError) + if err != nil { + return *issues, resp, err + } + return *issues, resp, githubError } func (s *IssueService) Create(owner, repo string, issueBody *IssueRequest) (*Issue, *http.Response, error) { issue := new(Issue) + githubError := new(GithubError) path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) - resp, err := s.sling.New().Post(path).JsonBody(issueBody).Receive(issue) - return issue, resp, err + resp, err := s.sling.New().Post(path).JsonBody(issueBody).Receive(issue, githubError) + if err != nil { + return issue, resp, err + } + return issue, resp, githubError } // (optional) Create a client to wrap services diff --git a/sling.go b/sling.go index 9d5b267..a185102 100644 --- a/sling.go +++ b/sling.go @@ -309,36 +309,64 @@ func addHeaders(req *http.Request, header http.Header) { // Sending -// Receive creates a new HTTP request, sends it, and decodes the response into -// the value pointed to by v. Receive is shorthand for calling Request and Do. -func (s *Sling) Receive(v interface{}) (*http.Response, error) { +// ReceiveSuccess creates a new HTTP request and returns the response. Success +// responses (2XX) are JSON decoded into the value pointed to by successV. +// Any error creating the request, sending it, or decoding a 2XX response +// is returned. +func (s *Sling) ReceiveSuccess(successV interface{}) (*http.Response, error) { + return s.Receive(successV, nil) +} + +// Receive creates a new HTTP request and returns the response. Success +// responses (2XX) are JSON decoded into the value pointed to by successV and +// other responses are JSON decoded into the value pointed to by failureV. +// Any error creating the request, sending it, or decoding the response is +// returned. +// Receive is shorthand for calling Request and Do. +func (s *Sling) Receive(successV, failureV interface{}) (*http.Response, error) { req, err := s.Request() if err != nil { return nil, err } - return s.Do(req, v) + return s.Do(req, successV, failureV) } -// Do sends the HTTP request and decodes the response into the value pointed -// to by v. It wraps http.Client.Do, but handles closing the Response Body. -// The Response and any error doing the request are returned. -// -// Note that non-2xx StatusCodes are valid responses, not errors. -func (s *Sling) Do(req *http.Request, v interface{}) (*http.Response, error) { +// Do sends an HTTP request and returns the response. Success responses (2XX) +// are JSON decoded into the value pointed to by successV and other responses +// are JSON decoded into the value pointed to by failureV. +// Any error sending the request or decoding the response is returned. +func (s *Sling) Do(req *http.Request, successV, failureV interface{}) (*http.Response, error) { resp, err := s.HttpClient.Do(req) if err != nil { return resp, err } // when err is nil, resp contains a non-nil resp.Body which must be closed defer resp.Body.Close() - if v != nil { - err = decodeResponse(resp, v) - } + err = decodeResponseJSON(resp, successV, failureV) return resp, err } -// decodeResponse decodes Response Body encoded as JSON into the value pointed -// to by v. Caller must provide non-nil v and close resp.Body once complete. -func decodeResponse(resp *http.Response, v interface{}) error { +// decodeResponse decodes response Body into the value pointed to by successV +// if the response is a success (2XX) or into the value pointed to by failureV +// otherwise. If the successV or failureV argument to decode into is nil, +// decoding is skipped. +// Caller is responsible for closing the resp.Body. +func decodeResponseJSON(resp *http.Response, successV, failureV interface{}) error { + if code := resp.StatusCode; 200 <= code && code <= 299 { + if successV != nil { + return decodeResponseBodyJSON(resp, successV) + } + } else { + if failureV != nil { + return decodeResponseBodyJSON(resp, failureV) + } + } + return nil +} + +// decodeJSONResponseBody JSON decodes a Response Body into the value pointed +// to by v. +// Caller must provide a non-nil v and close the resp.Body. +func decodeResponseBodyJSON(resp *http.Response, v interface{}) error { return json.NewDecoder(resp.Body).Decode(v) } diff --git a/sling_test.go b/sling_test.go index f3859bd..525618c 100644 --- a/sling_test.go +++ b/sling_test.go @@ -518,7 +518,7 @@ func TestDo(t *testing.T) { sling := New().Client(client) req, _ := http.NewRequest("GET", server.URL, nil) var model FakeModel - resp, err := sling.Do(req, &model) + resp, err := sling.Do(req, &model, nil) if err != nil { t.Errorf("expected nil, got %v", err) @@ -544,7 +544,7 @@ func TestDo_nilV(t *testing.T) { sling := New().Client(client) req, _ := http.NewRequest("GET", server.URL, nil) - resp, err := sling.Do(req, nil) + resp, err := sling.Do(req, nil, nil) if err != nil { t.Errorf("expected nil, got %v", err) From 48c2d98479e9f0b032b0d1478b6ca8ee4236ae49 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Sun, 17 May 2015 22:13:03 -0700 Subject: [PATCH 02/11] Add tests for Do method with success and failure responses --- sling_test.go | 129 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 28 deletions(-) diff --git a/sling_test.go b/sling_test.go index 525618c..2d60e99 100644 --- a/sling_test.go +++ b/sling_test.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "io/ioutil" "math" "net/http" "net/http/httptest" @@ -509,16 +508,30 @@ func TestAddQueryStructs(t *testing.T) { } } -func TestDo(t *testing.T) { - expectedText := "Some text" - var expectedFavoriteCount int64 = 24 - client, server := mockServer(`{"text":"Some text","favorite_count":24}`) +// Sending + +type APIError struct { + Message string `json:"message"` + Code int `json:"code"` +} + +func TestDo_onSuccess(t *testing.T) { + const expectedText = "Some text" + const expectedFavoriteCount int64 = 24 + + client, mux, server := testServer() defer server.Close() + mux.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`) + }) sling := New().Client(client) - req, _ := http.NewRequest("GET", server.URL, nil) - var model FakeModel - resp, err := sling.Do(req, &model, nil) + req, _ := http.NewRequest("GET", "http://example.com/success", nil) + + model := new(FakeModel) + apiError := new(APIError) + resp, err := sling.Do(req, model, apiError) if err != nil { t.Errorf("expected nil, got %v", err) @@ -526,10 +539,6 @@ func TestDo(t *testing.T) { if resp.StatusCode != 200 { t.Errorf("expected %d, got %d", 200, resp.StatusCode) } - expectedReadError := "http: read on closed response body" - if _, err = ioutil.ReadAll(resp.Body); err == nil || err.Error() != expectedReadError { - t.Errorf("expected %s, got %v", expectedReadError, err) - } if model.Text != expectedText { t.Errorf("expected %s, got %s", expectedText, model.Text) } @@ -538,13 +547,19 @@ func TestDo(t *testing.T) { } } -func TestDo_nilV(t *testing.T) { - client, server := mockServer("") +func TestDo_onSuccessWithNilValue(t *testing.T) { + client, mux, server := testServer() defer server.Close() + mux.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`) + }) sling := New().Client(client) - req, _ := http.NewRequest("GET", server.URL, nil) - resp, err := sling.Do(req, nil, nil) + req, _ := http.NewRequest("GET", "http://example.com/success", nil) + + apiError := new(APIError) + resp, err := sling.Do(req, nil, apiError) if err != nil { t.Errorf("expected nil, got %v", err) @@ -552,27 +567,85 @@ func TestDo_nilV(t *testing.T) { if resp.StatusCode != 200 { t.Errorf("expected %d, got %d", 200, resp.StatusCode) } - expectedReadError := "http: read on closed response body" - if _, err = ioutil.ReadAll(resp.Body); err == nil || err.Error() != expectedReadError { - t.Errorf("expected %s, got %v", expectedReadError, err) + expected := &APIError{} + if !reflect.DeepEqual(expected, apiError) { + t.Errorf("failureV should not be populated, exepcted %v, got %v", expected, apiError) } } -// Testing Utils +func TestDo_onFailure(t *testing.T) { + const expectedMessage = "Invalid argument" + const expectedCode int = 215 + + client, mux, server := testServer() + defer server.Close() + mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(400) + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"message": "Invalid argument", "code": 215}`) + }) + + sling := New().Client(client) + req, _ := http.NewRequest("GET", "http://example.com/failure", nil) + + model := new(FakeModel) + apiError := new(APIError) + resp, err := sling.Do(req, model, apiError) + + if err != nil { + t.Errorf("expected nil, got %v", err) + } + if resp.StatusCode != 400 { + t.Errorf("expected %d, got %d", 400, resp.StatusCode) + } + if apiError.Message != expectedMessage { + t.Errorf("expected %s, got %s", expectedMessage, apiError.Message) + } + if apiError.Code != expectedCode { + t.Errorf("expected %d, got %d", expectedCode, apiError.Code) + } +} -// mockServer returns an httptest.Server which always returns Responses with -// the given string as the Body with Content-Type application/json. -// The caller must close the test server. -func mockServer(body string) (*http.Client, *httptest.Server) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +func TestDo_onFailureWithNilValue(t *testing.T) { + client, mux, server := testServer() + defer server.Close() + mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(420) w.Header().Set("Content-Type", "application/json") - fmt.Fprintln(w, body) - })) + fmt.Fprintf(w, `{"message": "Enhance your calm", "code": 88}`) + }) + + sling := New().Client(client) + req, _ := http.NewRequest("GET", "http://example.com/failure", nil) + + model := new(FakeModel) + resp, err := sling.Do(req, model, nil) + + if err != nil { + t.Errorf("expected nil, got %v", err) + } + if resp.StatusCode != 420 { + t.Errorf("expected %d, got %d", 420, resp.StatusCode) + } + expected := &FakeModel{} + if !reflect.DeepEqual(expected, model) { + t.Errorf("successV should not be populated, exepcted %v, got %v", expected, model) + } +} + +// Testing Utils + +// testServer returns an http Client, ServeMux, and Server. The client proxies +// requests to the server and handlers can be registered on the mux to handle +// requests. The caller must close the test server. +func testServer() (*http.Client, *http.ServeMux, *httptest.Server) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) transport := &http.Transport{ Proxy: func(req *http.Request) (*url.URL, error) { return url.Parse(server.URL) }, } client := &http.Client{Transport: transport} - return client, server + return client, mux, server } From 7b74bcd1feee7c75c719c24ad0d8164559b5275c Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Sun, 17 May 2015 23:22:15 -0700 Subject: [PATCH 03/11] Add tests for Receive method with success and failure responses --- sling_test.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/sling_test.go b/sling_test.go index 2d60e99..77176a5 100644 --- a/sling_test.go +++ b/sling_test.go @@ -633,6 +633,84 @@ func TestDo_onFailureWithNilValue(t *testing.T) { } } +func TestReceive_success(t *testing.T) { + client, mux, server := testServer() + defer server.Close() + mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertQuery(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r) + assertPostForm(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r) + fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`) + }) + + endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Post("submit") + // encode url-tagged struct in query params and as post body for testing purposes + params := FakeParams{KindName: "vanilla", Count: 11} + model := new(FakeModel) + apiError := new(APIError) + resp, err := endpoint.New().QueryStruct(params).BodyStruct(params).Receive(model, apiError) + + if err != nil { + t.Errorf("expected nil, got %v", err) + } + if resp.StatusCode != 200 { + t.Errorf("expected %d, got %d", 200, resp.StatusCode) + } + expectedModel := &FakeModel{Text: "Some text", FavoriteCount: 24} + if !reflect.DeepEqual(expectedModel, model) { + t.Errorf("expected %v, got %v", expectedModel, model) + } + expectedAPIError := &APIError{} + if !reflect.DeepEqual(expectedAPIError, apiError) { + t.Errorf("failureV should be zero valued, exepcted %v, got %v", expectedAPIError, apiError) + } +} + +func TestReceive_failure(t *testing.T) { + client, mux, server := testServer() + defer server.Close() + mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) { + assertMethod(t, "POST", r) + assertQuery(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r) + assertPostForm(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r) + w.WriteHeader(429) + fmt.Fprintf(w, `{"message": "Rate limit exceeded", "code": 88}`) + }) + + endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Post("submit") + // encode url-tagged struct in query params and as post body for testing purposes + params := FakeParams{KindName: "vanilla", Count: 11} + model := new(FakeModel) + apiError := new(APIError) + resp, err := endpoint.New().QueryStruct(params).BodyStruct(params).Receive(model, apiError) + + if err != nil { + t.Errorf("expected nil, got %v", err) + } + if resp.StatusCode != 429 { + t.Errorf("expected %d, got %d", 429, resp.StatusCode) + } + expectedAPIError := &APIError{Message: "Rate limit exceeded", Code: 88} + if !reflect.DeepEqual(expectedAPIError, apiError) { + t.Errorf("expected %v, got %v", expectedAPIError, apiError) + } + expectedModel := &FakeModel{} + if !reflect.DeepEqual(expectedModel, model) { + t.Errorf("successV should not be zero valued, expected %v, got %v", expectedModel, model) + } +} + +func TestReceive_errorCreatingRequest(t *testing.T) { + expectedErr := errors.New("json: unsupported value: +Inf") + resp, err := New().JsonBody(FakeModel{Temperature: math.Inf(1)}).Receive(nil, nil) + if err == nil || err.Error() != expectedErr.Error() { + t.Errorf("expected %v, got %v", expectedErr, err) + } + if resp != nil { + t.Errorf("expected nil resp, got %v", resp) + } +} + // Testing Utils // testServer returns an http Client, ServeMux, and Server. The client proxies @@ -649,3 +727,34 @@ func testServer() (*http.Client, *http.ServeMux, *httptest.Server) { client := &http.Client{Transport: transport} return client, mux, server } + +func assertMethod(t *testing.T, expectedMethod string, req *http.Request) { + if actualMethod := req.Method; actualMethod != expectedMethod { + t.Errorf("expected method %s, got %s", expectedMethod, actualMethod) + } +} + +// assertQuery tests that the Request has the expected url query key/val pairs +func assertQuery(t *testing.T, expected map[string]string, req *http.Request) { + queryValues := req.URL.Query() // net/url Values is a map[string][]string + expectedValues := url.Values{} + for key, value := range expected { + expectedValues.Add(key, value) + } + if !reflect.DeepEqual(expectedValues, queryValues) { + t.Errorf("expected parameters %v, got %v", expected, req.URL.RawQuery) + } +} + +// assertPostForm tests that the Request has the expected key values pairs url +// encoded in its Body +func assertPostForm(t *testing.T, expected map[string]string, req *http.Request) { + req.ParseForm() // parses request Body to put url.Values in r.Form/r.PostForm + expectedValues := url.Values{} + for key, value := range expected { + expectedValues.Add(key, value) + } + if !reflect.DeepEqual(expectedValues, req.PostForm) { + t.Errorf("expected parameters %v, got %v", expected, req.PostForm) + } +} From bbc4ffdbfca5021aaee1b66953a2dffb561822c1 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Tue, 19 May 2015 21:17:15 -0700 Subject: [PATCH 04/11] Simplify oauth2 example and make example pass golint --- README.md | 19 +++++----- examples/github.go | 90 +++++++++++++++++++++++----------------------- 2 files changed, 53 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 781610b..738fabe 100644 --- a/README.md +++ b/README.md @@ -183,21 +183,18 @@ type IssueService struct { func NewIssueService(httpClient *http.Client) *IssueService { return &IssueService{ - sling: sling.New().Client(httpClient).Base(baseUrl), + sling: sling.New().Client(httpClient).Base(baseURL), } } -func (s *IssueService) Create(owner, repo string, issueBody *IssueRequest) (*Issue, *http.Response, error) { - issue := new(Issue) +func (s *IssueService) ListByRepo(owner, repo string, params *IssueListParams) ([]Issue, *http.Response, error) { + issues := new([]Issue) + githubError := new(GithubError) path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) - resp, err := s.sling.New().Post(path).JsonBody(issueBody).ReceiveSuccess(issue) - return issue, resp, err -} - -func (srvc IssueService) List(owner, repo string, params *IssueParams) ([]Issue, *http.Response, error) { - var issues []Issue - path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) - resp, err := srvc.sling.New().Get(path).QueryStruct(params).ReceiveSuccess(&issues) + resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(issues, githubError) + if err == nil { + err = githubError + } return *issues, resp, err } ``` diff --git a/examples/github.go b/examples/github.go index 1622bcc..e6be4b1 100644 --- a/examples/github.go +++ b/examples/github.go @@ -8,20 +8,39 @@ import ( "os" ) -const baseUrl = "https://api.github.com/" +const baseURL = "https://api.github.com/" // Define models -// Simplified https://developer.github.com/v3/issues/#response +// Issue is a simplified Github issue +// https://developer.github.com/v3/issues/#response type Issue struct { - Id int `json:"id"` - Url string `json:"url"` + ID int `json:"id"` + URL string `json:"url"` Number int `json:"number"` State string `json:"state"` Title string `json:"title"` Body string `json:"body"` } +// GithubError represents a Github API error response +// https://developer.github.com/v3/#client-errors +type GithubError struct { + Message string `json:"message"` + Errors []struct { + Resource string `json:"resource"` + Field string `json:"field"` + Code string `json:"code"` + } `json:"errors"` + DocumentationURL string `json:"documentation_url"` +} + +func (e GithubError) Error() string { + return fmt.Sprintf("github: %v %+v %v", e.Message, e.Errors, e.DocumentationURL) +} + +// IssueRequest is a simplified issue request +// https://developer.github.com/v3/issues/#create-an-issue type IssueRequest struct { Title string `json:"title,omitempty"` Body string `json:"body,omitempty"` @@ -30,6 +49,7 @@ type IssueRequest struct { Labels []string `json:"labels,omitempty"` } +// IssueListParams are the params for IssueService.List // https://developer.github.com/v3/issues/#parameters type IssueListParams struct { Filter string `url:"filter,omitempty"` @@ -40,80 +60,70 @@ type IssueListParams struct { Since string `url:"since,omitempty"` } -// https://developer.github.com/v3/#client-errors -type GithubError struct { - Message string `json:"message"` - Errors []struct { - Resource string `json:"resource"` - Field string `json:"field"` - Code string `json:"code"` - } `json:"errors"` - DocumentationURL string `json:"documentation_url"` -} - -func (e GithubError) Error() string { - return fmt.Sprintf("github: %v %+v %v", e.Message, e.Errors, e.DocumentationURL) -} - // Implement services +// IssueService provides methods for creating and reading issues. type IssueService struct { sling *sling.Sling } +// NewIssueService returns a new IssueService. func NewIssueService(httpClient *http.Client) *IssueService { return &IssueService{ - sling: sling.New().Client(httpClient).Base(baseUrl), + sling: sling.New().Client(httpClient).Base(baseURL), } } +// List returns the authenticated user's issues across repos and orgs. func (s *IssueService) List(params *IssueListParams) ([]Issue, *http.Response, error) { issues := new([]Issue) githubError := new(GithubError) resp, err := s.sling.New().Path("issues").QueryStruct(params).Receive(issues, githubError) - if err != nil { - return *issues, resp, err + if err == nil { + err = githubError } - return *issues, resp, githubError + return *issues, resp, err } +// ListByRepo returns a repository's issues. func (s *IssueService) ListByRepo(owner, repo string, params *IssueListParams) ([]Issue, *http.Response, error) { issues := new([]Issue) githubError := new(GithubError) path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(issues, githubError) - if err != nil { - return *issues, resp, err + if err == nil { + err = githubError } - return *issues, resp, githubError + return *issues, resp, err } +// Create creates a new issue on the specified repository. func (s *IssueService) Create(owner, repo string, issueBody *IssueRequest) (*Issue, *http.Response, error) { issue := new(Issue) githubError := new(GithubError) path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) resp, err := s.sling.New().Post(path).JsonBody(issueBody).Receive(issue, githubError) - if err != nil { - return issue, resp, err + if err == nil { + err = githubError } - return issue, resp, githubError + return issue, resp, err } // (optional) Create a client to wrap services -// Tiny Github client +// Client is a tiny Github client type Client struct { IssueService *IssueService // other service endpoints... } +// NewClient returns a new Client func NewClient(httpClient *http.Client) *Client { return &Client{ IssueService: NewIssueService(httpClient), } } -// example use of the tiny Github API func main() { // Github Unauthenticated API @@ -129,10 +139,9 @@ func main() { os.Exit(0) } - ts := &tokenSource{ - &oauth2.Token{AccessToken: accessToken}, - } - httpClient := oauth2.NewClient(oauth2.NoContext, ts) + config := &oauth2.Config{} + token := &oauth2.Token{AccessToken: accessToken} + httpClient := config.Client(oauth2.NoContext, token) client = NewClient(httpClient) issues, _, _ = client.IssueService.List(params) @@ -142,15 +151,6 @@ func main() { // Title: "Test title", // Body: "Some test issue", // } - // issue, _, _ := client.IssueService.Create("username", "myrepo", body) + // issue, _, _ := client.IssueService.Create("dghubble", "temp", body) // fmt.Println(issue) } - -// for using golang/oauth2 -type tokenSource struct { - token *oauth2.Token -} - -func (t *tokenSource) Token() (*oauth2.Token, error) { - return t.token, nil -} From 1436ba6163ed65c24b0f54c204375ce7af3fcc44 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Tue, 19 May 2015 22:32:47 -0700 Subject: [PATCH 05/11] Breaking name changes for consistency and golint compliance * Rename BodyStruct setter to BodyForm * Rename JsonBody setter to BodyJSON * Internalize Sling.RawUrl field to rawURL * Internalize Sling.Method field to method * Add .travis.yml config which runs golint * Remove exported GET, HEAD, etc. constants --- .travis.yml | 4 +- README.md | 16 ++--- examples/github.go | 2 +- sling.go | 113 +++++++++++++++--------------- sling_test.go | 166 ++++++++++++++++++++++----------------------- 5 files changed, 148 insertions(+), 153 deletions(-) diff --git a/.travis.yml b/.travis.yml index 924e56f..0c7aa5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,10 @@ go: - tip before_install: - go get golang.org/x/tools/cmd/vet + - go get github.com/golang/lint/golint install: - go get -v . script: - go test -v . - - go vet ./... \ No newline at end of file + - go vet ./... + - golint ./... \ No newline at end of file diff --git a/README.md b/README.md index 738fabe..5cf56d4 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Slings store HTTP Request properties to simplify sending requests and decoding r * Base/Path - path extend a Sling for different endpoints * Method Setters: Get/Post/Put/Patch/Delete/Head * Add and Set Request Headers -* Encode url structs into URL query parameters -* Encode json or a form into the Request Body +* Encode structs into URL query parameters +* Encode a form or JSON into the Request Body * Receive JSON success or failure responses ## Install @@ -85,9 +85,9 @@ req, err := githubBase.New().Get(path).QueryStruct(params).Request() ### Body -#### JsonBody +#### Json Body -Make a Sling include JSON in the Body of its Requests using `JsonBody`. +Make a Sling include JSON in the Body of its Requests using `BodyJSON`. ```go type IssueRequest struct { @@ -107,14 +107,14 @@ body := &IssueRequest{ Title: "Test title", Body: "Some issue", } -req, err := githubBase.New().Post(path).JsonBody(body).Request() +req, err := githubBase.New().Post(path).BodyJSON(body).Request() ``` Requests will include an `application/json` Content-Type header. -#### BodyStruct +#### Form Body -Make a Sling include a url-tagged struct as a url-encoded form in the Body of its Requests using `BodyStruct`. +Make a Sling include a url-tagged struct as a url-encoded form in the Body of its Requests using `BodyForm`. ```go type StatusUpdateParams struct { @@ -126,7 +126,7 @@ type StatusUpdateParams struct { ```go tweetParams := &StatusUpdateParams{Status: "writing some Go"} -req, err := twitterBase.New().Post(path).BodyStruct(tweetParams).Request() +req, err := twitterBase.New().Post(path).BodyForm(tweetParams).Request() ``` Requests will include an `application/x-www-form-urlencoded` Content-Type header. diff --git a/examples/github.go b/examples/github.go index e6be4b1..86cd042 100644 --- a/examples/github.go +++ b/examples/github.go @@ -102,7 +102,7 @@ func (s *IssueService) Create(owner, repo string, issueBody *IssueRequest) (*Iss issue := new(Issue) githubError := new(GithubError) path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) - resp, err := s.sling.New().Post(path).JsonBody(issueBody).Receive(issue, githubError) + resp, err := s.sling.New().Post(path).BodyJSON(issueBody).Receive(issue, githubError) if err == nil { err = githubError } diff --git a/sling.go b/sling.go index a185102..03fb826 100644 --- a/sling.go +++ b/sling.go @@ -11,12 +11,6 @@ import ( ) const ( - HEAD = "HEAD" - GET = "GET" - POST = "POST" - PUT = "PUT" - PATCH = "PATCH" - DELETE = "DELETE" contentType = "Content-Type" jsonContentType = "application/json" formContentType = "application/x-www-form-urlencoded" @@ -25,25 +19,25 @@ const ( // Sling is an HTTP Request builder and sender. type Sling struct { // http Client for doing requests - HttpClient *http.Client + HTTPClient *http.Client // HTTP method (GET, POST, etc.) - Method string + method string // raw url string for requests - RawUrl string + rawURL string // stores key-values pairs to add to request's Headers Header http.Header // url tagged query structs queryStructs []interface{} // json tagged body struct - jsonBody interface{} + bodyJSON interface{} // url tagged body struct (form) - bodyStruct interface{} + bodyForm interface{} } // New returns a new Sling with an http DefaultClient. func New() *Sling { return &Sling{ - HttpClient: http.DefaultClient, + HTTPClient: http.DefaultClient, Header: make(http.Header), queryStructs: make([]interface{}, 0), } @@ -59,9 +53,8 @@ func New() *Sling { // fooSling and barSling will both use the same client, but send requests to // https://api.io/foo/ and https://api.io/bar/ respectively. // -// Note that jsonBody and queryStructs item values are copied so if pointer -// values are used, mutating the original value will mutate the value within -// the child Sling. +// Note that query and body values are copied so if pointer values are used, +// mutating the original value will mutate the value within the child Sling. func (s *Sling) New() *Sling { // copy Headers pairs into new Header map headerCopy := make(http.Header) @@ -69,13 +62,13 @@ func (s *Sling) New() *Sling { headerCopy[k] = v } return &Sling{ - HttpClient: s.HttpClient, - Method: s.Method, - RawUrl: s.RawUrl, + HTTPClient: s.HTTPClient, + method: s.method, + rawURL: s.rawURL, Header: headerCopy, queryStructs: append([]interface{}{}, s.queryStructs...), - jsonBody: s.jsonBody, - bodyStruct: s.bodyStruct, + bodyJSON: s.bodyJSON, + bodyForm: s.bodyForm, } } @@ -85,9 +78,9 @@ func (s *Sling) New() *Sling { // the http.DefaultClient will be used. func (s *Sling) Client(httpClient *http.Client) *Sling { if httpClient == nil { - s.HttpClient = http.DefaultClient + s.HTTPClient = http.DefaultClient } else { - s.HttpClient = httpClient + s.HTTPClient = httpClient } return s } @@ -96,37 +89,37 @@ func (s *Sling) Client(httpClient *http.Client) *Sling { // Head sets the Sling method to HEAD and sets the given pathURL. func (s *Sling) Head(pathURL string) *Sling { - s.Method = HEAD + s.method = "HEAD" return s.Path(pathURL) } // Get sets the Sling method to GET and sets the given pathURL. func (s *Sling) Get(pathURL string) *Sling { - s.Method = GET + s.method = "GET" return s.Path(pathURL) } // Post sets the Sling method to POST and sets the given pathURL. func (s *Sling) Post(pathURL string) *Sling { - s.Method = POST + s.method = "POST" return s.Path(pathURL) } // Put sets the Sling method to PUT and sets the given pathURL. func (s *Sling) Put(pathURL string) *Sling { - s.Method = PUT + s.method = "PUT" return s.Path(pathURL) } // Patch sets the Sling method to PATCH and sets the given pathURL. func (s *Sling) Patch(pathURL string) *Sling { - s.Method = PATCH + s.method = "PATCH" return s.Path(pathURL) } // Delete sets the Sling method to DELETE and sets the given pathURL. func (s *Sling) Delete(pathURL string) *Sling { - s.Method = DELETE + s.method = "DELETE" return s.Path(pathURL) } @@ -148,20 +141,20 @@ func (s *Sling) Set(key, value string) *Sling { // Url -// Base sets the RawUrl. If you intend to extend the url with Path, +// Base sets the rawURL. If you intend to extend the url with Path, // baseUrl should be specified with a trailing slash. func (s *Sling) Base(rawURL string) *Sling { - s.RawUrl = rawURL + s.rawURL = rawURL return s } -// Path extends the RawUrl with the given path by resolving the reference to -// an absolute URL. If parsing errors occur, the RawUrl is left unmodified. +// Path extends the rawURL with the given path by resolving the reference to +// an absolute URL. If parsing errors occur, the rawURL is left unmodified. func (s *Sling) Path(path string) *Sling { - baseURL, baseErr := url.Parse(s.RawUrl) + baseURL, baseErr := url.Parse(s.rawURL) pathURL, pathErr := url.Parse(path) if baseErr == nil && pathErr == nil { - s.RawUrl = baseURL.ResolveReference(pathURL).String() + s.rawURL = baseURL.ResolveReference(pathURL).String() return s } return s @@ -181,25 +174,25 @@ func (s *Sling) QueryStruct(queryStruct interface{}) *Sling { // Body -// JsonBody sets the Sling's jsonBody. The value pointed to by the jsonBody +// BodyJSON sets the Sling's bodyJSON. The value pointed to by the bodyJSON // will be JSON encoded to set the Body on new requests (see Request()). -// The jsonBody argument should be a pointer to a json tagged struct. See +// The bodyJSON argument should be a pointer to a JSON tagged struct. See // https://golang.org/pkg/encoding/json/#MarshalIndent for details. -func (s *Sling) JsonBody(jsonBody interface{}) *Sling { - if jsonBody != nil { - s.jsonBody = jsonBody +func (s *Sling) BodyJSON(bodyJSON interface{}) *Sling { + if bodyJSON != nil { + s.bodyJSON = bodyJSON s.Set(contentType, jsonContentType) } return s } -// BodyStruct sets the Sling's bodyStruct. The value pointed to by the -// bodyStruct will be url encoded to set the Body on new requests. +// BodyForm sets the Sling's bodyForm. The value pointed to by the +// bodyForm will be url encoded to set the Body on new requests. // The bodyStruct argument should be a pointer to a url tagged struct. See // https://godoc.org/github.com/google/go-querystring/query for details. -func (s *Sling) BodyStruct(bodyStruct interface{}) *Sling { - if bodyStruct != nil { - s.bodyStruct = bodyStruct +func (s *Sling) BodyForm(bodyForm interface{}) *Sling { + if bodyForm != nil { + s.bodyForm = bodyForm s.Set(contentType, formContentType) } return s @@ -208,10 +201,10 @@ func (s *Sling) BodyStruct(bodyStruct interface{}) *Sling { // Requests // Request returns a new http.Request created with the Sling properties. -// Returns any errors parsing the RawUrl, encoding query structs, encoding +// Returns any errors parsing the rawURL, encoding query structs, encoding // the body, or creating the http.Request. func (s *Sling) Request() (*http.Request, error) { - reqURL, err := url.Parse(s.RawUrl) + reqURL, err := url.Parse(s.rawURL) if err != nil { return nil, err } @@ -223,7 +216,7 @@ func (s *Sling) Request() (*http.Request, error) { if err != nil { return nil, err } - req, err := http.NewRequest(s.Method, reqURL.String(), body) + req, err := http.NewRequest(s.method, reqURL.String(), body) if err != nil { return nil, err } @@ -259,13 +252,13 @@ func addQueryStructs(reqURL *url.URL, queryStructs []interface{}) error { // getRequestBody returns the io.Reader which should be used as the body // of new Requests. func (s *Sling) getRequestBody() (body io.Reader, err error) { - if s.jsonBody != nil && s.Header.Get(contentType) == jsonContentType { - body, err = encodeJSONBody(s.jsonBody) + if s.bodyJSON != nil && s.Header.Get(contentType) == jsonContentType { + body, err = encodeBodyJSON(s.bodyJSON) if err != nil { return nil, err } - } else if s.bodyStruct != nil && s.Header.Get(contentType) == formContentType { - body, err = encodeBodyStruct(s.bodyStruct) + } else if s.bodyForm != nil && s.Header.Get(contentType) == formContentType { + body, err = encodeBodyForm(s.bodyForm) if err != nil { return nil, err } @@ -273,13 +266,13 @@ func (s *Sling) getRequestBody() (body io.Reader, err error) { return body, nil } -// encodeJSONBody JSON encodes the value pointed to by jsonBody into an +// encodeBodyJSON JSON encodes the value pointed to by bodyJSON into an // io.Reader, typically for use as a Request Body. -func encodeJSONBody(jsonBody interface{}) (io.Reader, error) { +func encodeBodyJSON(bodyJSON interface{}) (io.Reader, error) { var buf = new(bytes.Buffer) - if jsonBody != nil { + if bodyJSON != nil { buf = &bytes.Buffer{} - err := json.NewEncoder(buf).Encode(jsonBody) + err := json.NewEncoder(buf).Encode(bodyJSON) if err != nil { return nil, err } @@ -287,10 +280,10 @@ func encodeJSONBody(jsonBody interface{}) (io.Reader, error) { return buf, nil } -// encodeBodyStruct url encodes the value pointed to by bodyStruct into an +// encodeBodyForm url encodes the value pointed to by bodyForm into an // io.Reader, typically for use as a Request Body. -func encodeBodyStruct(bodyStruct interface{}) (io.Reader, error) { - values, err := goquery.Values(bodyStruct) +func encodeBodyForm(bodyForm interface{}) (io.Reader, error) { + values, err := goquery.Values(bodyForm) if err != nil { return nil, err } @@ -336,7 +329,7 @@ func (s *Sling) Receive(successV, failureV interface{}) (*http.Response, error) // are JSON decoded into the value pointed to by failureV. // Any error sending the request or decoding the response is returned. func (s *Sling) Do(req *http.Request, successV, failureV interface{}) (*http.Response, error) { - resp, err := s.HttpClient.Do(req) + resp, err := s.HTTPClient.Do(req) if err != nil { return resp, err } @@ -364,7 +357,7 @@ func decodeResponseJSON(resp *http.Response, successV, failureV interface{}) err return nil } -// decodeJSONResponseBody JSON decodes a Response Body into the value pointed +// decodeResponseBodyJSON JSON decodes a Response Body into the value pointed // to by v. // Caller must provide a non-nil v and close the resp.Body. func decodeResponseBodyJSON(resp *http.Response, v interface{}) error { diff --git a/sling_test.go b/sling_test.go index 77176a5..0236637 100644 --- a/sling_test.go +++ b/sling_test.go @@ -36,8 +36,8 @@ var modelA = FakeModel{Text: "note", FavoriteCount: 12} func TestNew(t *testing.T) { sling := New() - if sling.HttpClient != http.DefaultClient { - t.Errorf("expected %v, got %v", http.DefaultClient, sling.HttpClient) + if sling.HTTPClient != http.DefaultClient { + t.Errorf("expected %v, got %v", http.DefaultClient, sling.HTTPClient) } if sling.Header == nil { t.Errorf("Header map not initialized with make") @@ -49,30 +49,30 @@ func TestNew(t *testing.T) { func TestSlingNew(t *testing.T) { cases := []*Sling{ - &Sling{HttpClient: &http.Client{}, Method: "GET", RawUrl: "http://example.com"}, - &Sling{HttpClient: nil, Method: "", RawUrl: "http://example.com"}, + &Sling{HTTPClient: &http.Client{}, method: "GET", rawURL: "http://example.com"}, + &Sling{HTTPClient: nil, method: "", rawURL: "http://example.com"}, &Sling{queryStructs: make([]interface{}, 0)}, &Sling{queryStructs: []interface{}{paramsA}}, &Sling{queryStructs: []interface{}{paramsA, paramsB}}, - &Sling{jsonBody: &FakeModel{Text: "a"}}, - &Sling{jsonBody: FakeModel{Text: "a"}}, - &Sling{jsonBody: nil}, + &Sling{bodyJSON: &FakeModel{Text: "a"}}, + &Sling{bodyJSON: FakeModel{Text: "a"}}, + &Sling{bodyJSON: nil}, New().Add("Content-Type", "application/json"), New().Add("A", "B").Add("a", "c").New(), New().Add("A", "B").New().Add("a", "c"), - New().BodyStruct(paramsB), - New().BodyStruct(paramsB).New(), + New().BodyForm(paramsB), + New().BodyForm(paramsB).New(), } for _, sling := range cases { child := sling.New() - if child.HttpClient != sling.HttpClient { - t.Errorf("expected %p, got %p", sling.HttpClient, child.HttpClient) + if child.HTTPClient != sling.HTTPClient { + t.Errorf("expected %p, got %p", sling.HTTPClient, child.HTTPClient) } - if child.Method != sling.Method { - t.Errorf("expected %s, got %s", sling.Method, child.Method) + if child.method != sling.method { + t.Errorf("expected %s, got %s", sling.method, child.method) } - if child.RawUrl != sling.RawUrl { - t.Errorf("expected %s, got %s", sling.RawUrl, child.RawUrl) + if child.rawURL != sling.rawURL { + t.Errorf("expected %s, got %s", sling.rawURL, child.rawURL) } // Header should be a copy of parent Sling Header. For example, calling // baseSling.Add("k","v") should not mutate previously created child Slings @@ -94,13 +94,13 @@ func TestSlingNew(t *testing.T) { t.Errorf("child.queryStructs was a re-slice, expected slice with copied contents") } } - // jsonBody should be copied - if child.jsonBody != sling.jsonBody { - t.Errorf("expected %v, got %v", sling.jsonBody, child.jsonBody) + // bodyJSON should be copied + if child.bodyJSON != sling.bodyJSON { + t.Errorf("expected %v, got %v", sling.bodyJSON, child.bodyJSON) } - // bodyStruct should be copied - if child.bodyStruct != sling.bodyStruct { - t.Errorf("expected %v, got %v", sling.bodyStruct, child.bodyStruct) + // bodyForm should be copied + if child.bodyForm != sling.bodyForm { + t.Errorf("expected %v, got %v", sling.bodyForm, child.bodyForm) } } } @@ -117,8 +117,8 @@ func TestClientSetter(t *testing.T) { for _, c := range cases { sling := New() sling.Client(c.input) - if sling.HttpClient != c.expected { - t.Errorf("expected %v, got %v", c.expected, sling.HttpClient) + if sling.HTTPClient != c.expected { + t.Errorf("expected %v, got %v", c.expected, sling.HTTPClient) } } } @@ -127,8 +127,8 @@ func TestBaseSetter(t *testing.T) { cases := []string{"http://a.io/", "http://b.io", "/path", "path", ""} for _, base := range cases { sling := New().Base(base) - if sling.RawUrl != base { - t.Errorf("expected %s, got %s", base, sling.RawUrl) + if sling.rawURL != base { + t.Errorf("expected %s, got %s", base, sling.rawURL) } } } @@ -144,7 +144,7 @@ func TestPathSetter(t *testing.T) { {"http://a.io", "foo", "http://a.io/foo"}, {"http://a.io", "/foo", "http://a.io/foo"}, {"http://a.io/foo/", "bar", "http://a.io/foo/bar"}, - // rawUrl should end in trailing slash if it is to be Path extended + // rawURL should end in trailing slash if it is to be Path extended {"http://a.io/foo", "bar", "http://a.io/bar"}, {"http://a.io/foo", "/bar", "http://a.io/bar"}, // path extension is absolute @@ -159,8 +159,8 @@ func TestPathSetter(t *testing.T) { } for _, c := range cases { sling := New().Base(c.rawURL).Path(c.path) - if sling.RawUrl != c.expectedRawURL { - t.Errorf("expected %s, got %s", c.expectedRawURL, sling.RawUrl) + if sling.rawURL != c.expectedRawURL { + t.Errorf("expected %s, got %s", c.expectedRawURL, sling.rawURL) } } } @@ -170,16 +170,16 @@ func TestMethodSetters(t *testing.T) { sling *Sling expectedMethod string }{ - {New().Head("http://a.io"), HEAD}, - {New().Get("http://a.io"), GET}, - {New().Post("http://a.io"), POST}, - {New().Put("http://a.io"), PUT}, - {New().Patch("http://a.io"), PATCH}, - {New().Delete("http://a.io"), DELETE}, + {New().Head("http://a.io"), "HEAD"}, + {New().Get("http://a.io"), "GET"}, + {New().Post("http://a.io"), "POST"}, + {New().Put("http://a.io"), "PUT"}, + {New().Patch("http://a.io"), "PATCH"}, + {New().Delete("http://a.io"), "DELETE"}, } for _, c := range cases { - if c.sling.Method != c.expectedMethod { - t.Errorf("expected method %s, got %s", c.expectedMethod, c.sling.Method) + if c.sling.method != c.expectedMethod { + t.Errorf("expected method %s, got %s", c.expectedMethod, c.sling.method) } } } @@ -258,28 +258,28 @@ func TestQueryStructSetter(t *testing.T) { } } -func TestJsonBodySetter(t *testing.T) { +func TestBodyJSONSetter(t *testing.T) { fakeModel := &FakeModel{} cases := []struct { initial interface{} input interface{} expected interface{} }{ - // json tagged struct is set as jsonBody + // json tagged struct is set as bodyJSON {nil, fakeModel, fakeModel}, - // nil argument to jsonBody does not replace existing jsonBody + // nil argument to bodyJSON does not replace existing bodyJSON {fakeModel, nil, fakeModel}, - // nil jsonBody remains nil + // nil bodyJSON remains nil {nil, nil, nil}, } for _, c := range cases { sling := New() - sling.jsonBody = c.initial - sling.JsonBody(c.input) - if sling.jsonBody != c.expected { - t.Errorf("expected %v, got %v", c.expected, sling.jsonBody) + sling.bodyJSON = c.initial + sling.BodyJSON(c.input) + if sling.bodyJSON != c.expected { + t.Errorf("expected %v, got %v", c.expected, sling.bodyJSON) } - // Header Content-Type should be application/json if jsonBody arg was non-nil + // Header Content-Type should be application/json if bodyJSON arg was non-nil if c.input != nil && sling.Header.Get(contentType) != jsonContentType { t.Errorf("Incorrect or missing header, expected %s, got %s", jsonContentType, sling.Header.Get(contentType)) } else if c.input == nil && sling.Header.Get(contentType) != "" { @@ -288,7 +288,7 @@ func TestJsonBodySetter(t *testing.T) { } } -func TestBodyStructSetter(t *testing.T) { +func TestBodyFormSetter(t *testing.T) { cases := []struct { initial interface{} input interface{} @@ -303,10 +303,10 @@ func TestBodyStructSetter(t *testing.T) { } for _, c := range cases { sling := New() - sling.bodyStruct = c.initial - sling.BodyStruct(c.input) - if sling.bodyStruct != c.expected { - t.Errorf("expected %v, got %v", c.expected, sling.bodyStruct) + sling.bodyForm = c.initial + sling.BodyForm(c.input) + if sling.bodyForm != c.expected { + t.Errorf("expected %v, got %v", c.expected, sling.bodyForm) } // Content-Type should be application/x-www-form-urlencoded if bodyStruct was non-nil if c.input != nil && sling.Header.Get(contentType) != formContentType { @@ -327,23 +327,23 @@ func TestRequest_urlAndMethod(t *testing.T) { }{ {New().Base("http://a.io"), "", "http://a.io", nil}, {New().Path("http://a.io"), "", "http://a.io", nil}, - {New().Get("http://a.io"), GET, "http://a.io", nil}, - {New().Put("http://a.io"), PUT, "http://a.io", nil}, + {New().Get("http://a.io"), "GET", "http://a.io", nil}, + {New().Put("http://a.io"), "PUT", "http://a.io", nil}, {New().Base("http://a.io/").Path("foo"), "", "http://a.io/foo", nil}, - {New().Base("http://a.io/").Post("foo"), POST, "http://a.io/foo", nil}, + {New().Base("http://a.io/").Post("foo"), "POST", "http://a.io/foo", nil}, // if relative path is an absolute url, base is ignored {New().Base("http://a.io").Path("http://b.io"), "", "http://b.io", nil}, {New().Path("http://a.io").Path("http://b.io"), "", "http://b.io", nil}, // last method setter takes priority - {New().Get("http://b.io").Post("http://a.io"), POST, "http://a.io", nil}, - {New().Post("http://a.io/").Put("foo/").Delete("bar"), DELETE, "http://a.io/foo/bar", nil}, + {New().Get("http://b.io").Post("http://a.io"), "POST", "http://a.io", nil}, + {New().Post("http://a.io/").Put("foo/").Delete("bar"), "DELETE", "http://a.io/foo/bar", nil}, // last Base setter takes priority {New().Base("http://a.io").Base("http://b.io"), "", "http://b.io", nil}, // Path setters are additive {New().Base("http://a.io/").Path("foo/").Path("bar"), "", "http://a.io/foo/bar", nil}, {New().Path("http://a.io/").Path("foo/").Path("bar"), "", "http://a.io/foo/bar", nil}, // removes extra '/' between base and ref url - {New().Base("http://a.io/").Get("/foo"), GET, "http://a.io/foo", nil}, + {New().Base("http://a.io/").Get("/foo"), "GET", "http://a.io/foo", nil}, } for _, c := range cases { req, err := c.sling.Request() @@ -384,24 +384,24 @@ func TestRequest_body(t *testing.T) { expectedBody string // expected Body io.Reader as a string expectedContentType string }{ - // JsonBody - {New().JsonBody(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType}, - {New().JsonBody(&modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType}, - {New().JsonBody(&FakeModel{}), "{}\n", jsonContentType}, - {New().JsonBody(FakeModel{}), "{}\n", jsonContentType}, - // JsonBody overrides existing values - {New().JsonBody(&FakeModel{}).JsonBody(&FakeModel{Text: "msg"}), "{\"text\":\"msg\"}\n", jsonContentType}, - // BodyStruct (form) - {New().BodyStruct(paramsA), "limit=30", formContentType}, - {New().BodyStruct(paramsB), "count=25&kind_name=recent", formContentType}, - {New().BodyStruct(¶msB), "count=25&kind_name=recent", formContentType}, - // BodyStruct overrides existing values - {New().BodyStruct(paramsA).New().BodyStruct(paramsB), "count=25&kind_name=recent", formContentType}, - // Mixture of JsonBody and BodyStruct prefers body setter called last with a non-nil argument - {New().BodyStruct(paramsB).New().JsonBody(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType}, - {New().JsonBody(modelA).New().BodyStruct(paramsB), "count=25&kind_name=recent", formContentType}, - {New().BodyStruct(paramsB).New().JsonBody(nil), "count=25&kind_name=recent", formContentType}, - {New().JsonBody(modelA).New().BodyStruct(nil), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType}, + // BodyJSON + {New().BodyJSON(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType}, + {New().BodyJSON(&modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType}, + {New().BodyJSON(&FakeModel{}), "{}\n", jsonContentType}, + {New().BodyJSON(FakeModel{}), "{}\n", jsonContentType}, + // BodyJSON overrides existing values + {New().BodyJSON(&FakeModel{}).BodyJSON(&FakeModel{Text: "msg"}), "{\"text\":\"msg\"}\n", jsonContentType}, + // BodyForm + {New().BodyForm(paramsA), "limit=30", formContentType}, + {New().BodyForm(paramsB), "count=25&kind_name=recent", formContentType}, + {New().BodyForm(¶msB), "count=25&kind_name=recent", formContentType}, + // BodyForm overrides existing values + {New().BodyForm(paramsA).New().BodyForm(paramsB), "count=25&kind_name=recent", formContentType}, + // Mixture of BodyJSON and BodyForm prefers body setter called last with a non-nil argument + {New().BodyForm(paramsB).New().BodyJSON(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType}, + {New().BodyJSON(modelA).New().BodyForm(paramsB), "count=25&kind_name=recent", formContentType}, + {New().BodyForm(paramsB).New().BodyJSON(nil), "count=25&kind_name=recent", formContentType}, + {New().BodyJSON(modelA).New().BodyForm(nil), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType}, } for _, c := range cases { req, _ := c.sling.Request() @@ -419,18 +419,18 @@ func TestRequest_body(t *testing.T) { } func TestRequest_bodyNoData(t *testing.T) { - // test that Body is left nil when no jsonBody or bodyStruct set + // test that Body is left nil when no bodyJSON or bodyStruct set slings := []*Sling{ New(), - New().JsonBody(nil), - New().BodyStruct(nil), + New().BodyJSON(nil), + New().BodyForm(nil), } for _, sling := range slings { req, _ := sling.Request() if req.Body != nil { t.Errorf("expected nil Request.Body, got %v", req.Body) } - // Header Content-Type should not be set when jsonBody argument was nil or never called + // Header Content-Type should not be set when bodyJSON argument was nil or never called if actualHeader := req.Header.Get(contentType); actualHeader != "" { t.Errorf("did not expect a Content-Type header, got %s", actualHeader) } @@ -443,7 +443,7 @@ func TestRequest_bodyEncodeErrors(t *testing.T) { expectedErr error }{ // check that Encode errors are propagated, illegal JSON field - {New().JsonBody(FakeModel{Temperature: math.Inf(1)}), errors.New("json: unsupported value: +Inf")}, + {New().BodyJSON(FakeModel{Temperature: math.Inf(1)}), errors.New("json: unsupported value: +Inf")}, } for _, c := range cases { req, err := c.sling.Request() @@ -496,7 +496,7 @@ func TestAddQueryStructs(t *testing.T) { {"http://a.io", []interface{}{paramsA}, "http://a.io?limit=30"}, {"http://a.io", []interface{}{paramsA, paramsA}, "http://a.io?limit=30&limit=30"}, {"http://a.io", []interface{}{paramsA, paramsB}, "http://a.io?count=25&kind_name=recent&limit=30"}, - // don't blow away query values on the RawUrl (parsed into RawQuery) + // don't blow away query values on the rawURL (parsed into RawQuery) {"http://a.io?initial=7", []interface{}{paramsA}, "http://a.io?initial=7&limit=30"}, } for _, c := range cases { @@ -648,7 +648,7 @@ func TestReceive_success(t *testing.T) { params := FakeParams{KindName: "vanilla", Count: 11} model := new(FakeModel) apiError := new(APIError) - resp, err := endpoint.New().QueryStruct(params).BodyStruct(params).Receive(model, apiError) + resp, err := endpoint.New().QueryStruct(params).BodyForm(params).Receive(model, apiError) if err != nil { t.Errorf("expected nil, got %v", err) @@ -682,7 +682,7 @@ func TestReceive_failure(t *testing.T) { params := FakeParams{KindName: "vanilla", Count: 11} model := new(FakeModel) apiError := new(APIError) - resp, err := endpoint.New().QueryStruct(params).BodyStruct(params).Receive(model, apiError) + resp, err := endpoint.New().QueryStruct(params).BodyForm(params).Receive(model, apiError) if err != nil { t.Errorf("expected nil, got %v", err) @@ -702,7 +702,7 @@ func TestReceive_failure(t *testing.T) { func TestReceive_errorCreatingRequest(t *testing.T) { expectedErr := errors.New("json: unsupported value: +Inf") - resp, err := New().JsonBody(FakeModel{Temperature: math.Inf(1)}).Receive(nil, nil) + resp, err := New().BodyJSON(FakeModel{Temperature: math.Inf(1)}).Receive(nil, nil) if err == nil || err.Error() != expectedErr.Error() { t.Errorf("expected %v, got %v", expectedErr, err) } From 73ff937f6d44f42badf9500d1d6373b2d3c21884 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Tue, 19 May 2015 23:12:53 -0700 Subject: [PATCH 06/11] Add CHANGES.md Changelog --- CHANGES.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 CHANGES.md diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..8ec28cc --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,47 @@ +# Sling Changelog + +Notable changes between releases. + +## v1.0.0-rc (unreleased) + +* Added support for receiving and decoding error JSON structs +* Renamed Sling `JsonBody` setter to `BodyJSON` (breaking) +* Renamed Sling `BodyStruct` setter to `BodyForm` (breaking) +* Renamed Sling fields `method`, `rawURL` to be internal (breaking) +* Changed `Sling.Receive(v interface{})` to `Sling.Receive(successV, failureV interface{})` (breaking) + * Previously `Receive` attempted to decode the response Body in all cases + * Updated `Receive` will decode the response Body into successV for 2XX responses or decode the Body into failureV for other status codes. Pass a nil `successV` or `failureV` to skip JSON decoding into that value. + * To upgrade, pass nil for the `failureV` argument or consider defining a JSON tagged struct appropriate for the API endpoint. (e.g. `s.Receive(&issue, nil)`, `s.Receive(&issue, &githubError)`) + * To retain the old behavior, duplicate the first argument (e.g. s.Receive(&tweet, &tweet)) +* Changed `Sling.Do(http.Request, v interface{})` to `Sling.Do(http.Request, successV, failureV interface{})` (breaking) + * See the changelog entry about `Receive`, the upgrade path is the same. +* Removed HEAD, GET, POST, PUT, PATCH, DELETE constants, no reason to export them (breaking) + +## v0.4.0 (2015-04-26) + +* Improved golint compliance +* Fixed typos and test printouts + +## v0.3.0 (2015-04-21) + +* Added BodyStruct method for setting a url encoded form body on the Request +* Added Add and Set methods for adding or setting Request Headers +* Added JsonBody method for setting JSON Request Body +* Improved examples and documentation + +## v0.2.0 (2015-04-05) + +* Added http.Client setter +* Added Sling.New() method to return a copy of a Sling +* Added Base setter and Path extension support +* Added method setters (Get, Post, Put, Patch, Delete, Head) +* Added support for encoding URL Query parameters +* Added example tiny Github API +* Changed v0.1.0 method signatures and names (breaking) +* Removed Go 1.0 support + +## v0.1.0 (2015-04-01) + +* Support decoding JSON responses. + + From e61868db225be8f30b248ae0c27a8a8cdc351ba4 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Fri, 22 May 2015 00:01:52 -0700 Subject: [PATCH 07/11] Check Content-Type is application/json before JSON decoding responses * Fix incorrect Content-Type headers used in test responses --- CHANGES.md | 1 + sling.go | 4 +++- sling_test.go | 26 ++++++++++++++++++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8ec28cc..95abb7e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Notable changes between releases. * To retain the old behavior, duplicate the first argument (e.g. s.Receive(&tweet, &tweet)) * Changed `Sling.Do(http.Request, v interface{})` to `Sling.Do(http.Request, successV, failureV interface{})` (breaking) * See the changelog entry about `Receive`, the upgrade path is the same. +* Require responses to have Content-Type containing application/json before attempting JSON decoding in `Recieve`, `ReceiveSuccess` or `Do`. * Removed HEAD, GET, POST, PUT, PATCH, DELETE constants, no reason to export them (breaking) ## v0.4.0 (2015-04-26) diff --git a/sling.go b/sling.go index 03fb826..fa20e5c 100644 --- a/sling.go +++ b/sling.go @@ -335,7 +335,9 @@ func (s *Sling) Do(req *http.Request, successV, failureV interface{}) (*http.Res } // when err is nil, resp contains a non-nil resp.Body which must be closed defer resp.Body.Close() - err = decodeResponseJSON(resp, successV, failureV) + if strings.Contains(resp.Header.Get(contentType), jsonContentType) { + err = decodeResponseJSON(resp, successV, failureV) + } return resp, err } diff --git a/sling_test.go b/sling_test.go index 0236637..91c80f7 100644 --- a/sling_test.go +++ b/sling_test.go @@ -580,8 +580,8 @@ func TestDo_onFailure(t *testing.T) { client, mux, server := testServer() defer server.Close() mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(400) w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) fmt.Fprintf(w, `{"message": "Invalid argument", "code": 215}`) }) @@ -610,8 +610,8 @@ func TestDo_onFailureWithNilValue(t *testing.T) { client, mux, server := testServer() defer server.Close() mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(420) w.Header().Set("Content-Type", "application/json") + w.WriteHeader(420) fmt.Fprintf(w, `{"message": "Enhance your calm", "code": 88}`) }) @@ -633,6 +633,26 @@ func TestDo_onFailureWithNilValue(t *testing.T) { } } +func TestDo_skipDecodingIfContentTypeWrong(t *testing.T) { + client, mux, server := testServer() + defer server.Close() + mux.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`) + }) + + sling := New().Client(client) + req, _ := http.NewRequest("GET", "http://example.com/success", nil) + + model := new(FakeModel) + sling.Do(req, model, nil) + + expectedModel := &FakeModel{} + if !reflect.DeepEqual(expectedModel, model) { + t.Errorf("decoding should have been skipped, Content-Type was incorrect") + } +} + func TestReceive_success(t *testing.T) { client, mux, server := testServer() defer server.Close() @@ -640,6 +660,7 @@ func TestReceive_success(t *testing.T) { assertMethod(t, "POST", r) assertQuery(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r) assertPostForm(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r) + w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`) }) @@ -673,6 +694,7 @@ func TestReceive_failure(t *testing.T) { assertMethod(t, "POST", r) assertQuery(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r) assertPostForm(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r) + w.Header().Set("Content-Type", "application/json") w.WriteHeader(429) fmt.Fprintf(w, `{"message": "Rate limit exceeded", "code": 88}`) }) From 1a5439105934c9416c1eab52e2e62822d6b3933e Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Fri, 22 May 2015 01:41:25 -0700 Subject: [PATCH 08/11] Internalize Sling HTTPClient and Header fields --- CHANGES.md | 2 +- sling.go | 30 +++++++++++++-------------- sling_test.go | 56 +++++++++++++++++++++++++-------------------------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 95abb7e..d8fda07 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,7 +7,7 @@ Notable changes between releases. * Added support for receiving and decoding error JSON structs * Renamed Sling `JsonBody` setter to `BodyJSON` (breaking) * Renamed Sling `BodyStruct` setter to `BodyForm` (breaking) -* Renamed Sling fields `method`, `rawURL` to be internal (breaking) +* Renamed Sling fields `httpClient`, `method`, `rawURL`, and `header` to be internal (breaking) * Changed `Sling.Receive(v interface{})` to `Sling.Receive(successV, failureV interface{})` (breaking) * Previously `Receive` attempted to decode the response Body in all cases * Updated `Receive` will decode the response Body into successV for 2XX responses or decode the Body into failureV for other status codes. Pass a nil `successV` or `failureV` to skip JSON decoding into that value. diff --git a/sling.go b/sling.go index fa20e5c..2196409 100644 --- a/sling.go +++ b/sling.go @@ -19,13 +19,13 @@ const ( // Sling is an HTTP Request builder and sender. type Sling struct { // http Client for doing requests - HTTPClient *http.Client + httpClient *http.Client // HTTP method (GET, POST, etc.) method string // raw url string for requests rawURL string // stores key-values pairs to add to request's Headers - Header http.Header + header http.Header // url tagged query structs queryStructs []interface{} // json tagged body struct @@ -37,8 +37,8 @@ type Sling struct { // New returns a new Sling with an http DefaultClient. func New() *Sling { return &Sling{ - HTTPClient: http.DefaultClient, - Header: make(http.Header), + httpClient: http.DefaultClient, + header: make(http.Header), queryStructs: make([]interface{}, 0), } } @@ -58,14 +58,14 @@ func New() *Sling { func (s *Sling) New() *Sling { // copy Headers pairs into new Header map headerCopy := make(http.Header) - for k, v := range s.Header { + for k, v := range s.header { headerCopy[k] = v } return &Sling{ - HTTPClient: s.HTTPClient, + httpClient: s.httpClient, method: s.method, rawURL: s.rawURL, - Header: headerCopy, + header: headerCopy, queryStructs: append([]interface{}{}, s.queryStructs...), bodyJSON: s.bodyJSON, bodyForm: s.bodyForm, @@ -78,9 +78,9 @@ func (s *Sling) New() *Sling { // the http.DefaultClient will be used. func (s *Sling) Client(httpClient *http.Client) *Sling { if httpClient == nil { - s.HTTPClient = http.DefaultClient + s.httpClient = http.DefaultClient } else { - s.HTTPClient = httpClient + s.httpClient = httpClient } return s } @@ -128,14 +128,14 @@ func (s *Sling) Delete(pathURL string) *Sling { // Add adds the key, value pair in Headers, appending values for existing keys // to the key's values. Header keys are canonicalized. func (s *Sling) Add(key, value string) *Sling { - s.Header.Add(key, value) + s.header.Add(key, value) return s } // Set sets the key, value pair in Headers, replacing existing values // associated with key. Header keys are canonicalized. func (s *Sling) Set(key, value string) *Sling { - s.Header.Set(key, value) + s.header.Set(key, value) return s } @@ -220,7 +220,7 @@ func (s *Sling) Request() (*http.Request, error) { if err != nil { return nil, err } - addHeaders(req, s.Header) + addHeaders(req, s.header) return req, err } @@ -252,12 +252,12 @@ func addQueryStructs(reqURL *url.URL, queryStructs []interface{}) error { // getRequestBody returns the io.Reader which should be used as the body // of new Requests. func (s *Sling) getRequestBody() (body io.Reader, err error) { - if s.bodyJSON != nil && s.Header.Get(contentType) == jsonContentType { + if s.bodyJSON != nil && s.header.Get(contentType) == jsonContentType { body, err = encodeBodyJSON(s.bodyJSON) if err != nil { return nil, err } - } else if s.bodyForm != nil && s.Header.Get(contentType) == formContentType { + } else if s.bodyForm != nil && s.header.Get(contentType) == formContentType { body, err = encodeBodyForm(s.bodyForm) if err != nil { return nil, err @@ -329,7 +329,7 @@ func (s *Sling) Receive(successV, failureV interface{}) (*http.Response, error) // are JSON decoded into the value pointed to by failureV. // Any error sending the request or decoding the response is returned. func (s *Sling) Do(req *http.Request, successV, failureV interface{}) (*http.Response, error) { - resp, err := s.HTTPClient.Do(req) + resp, err := s.httpClient.Do(req) if err != nil { return resp, err } diff --git a/sling_test.go b/sling_test.go index 91c80f7..c8d31a8 100644 --- a/sling_test.go +++ b/sling_test.go @@ -36,10 +36,10 @@ var modelA = FakeModel{Text: "note", FavoriteCount: 12} func TestNew(t *testing.T) { sling := New() - if sling.HTTPClient != http.DefaultClient { - t.Errorf("expected %v, got %v", http.DefaultClient, sling.HTTPClient) + if sling.httpClient != http.DefaultClient { + t.Errorf("expected %v, got %v", http.DefaultClient, sling.httpClient) } - if sling.Header == nil { + if sling.header == nil { t.Errorf("Header map not initialized with make") } if sling.queryStructs == nil { @@ -49,8 +49,8 @@ func TestNew(t *testing.T) { func TestSlingNew(t *testing.T) { cases := []*Sling{ - &Sling{HTTPClient: &http.Client{}, method: "GET", rawURL: "http://example.com"}, - &Sling{HTTPClient: nil, method: "", rawURL: "http://example.com"}, + &Sling{httpClient: &http.Client{}, method: "GET", rawURL: "http://example.com"}, + &Sling{httpClient: nil, method: "", rawURL: "http://example.com"}, &Sling{queryStructs: make([]interface{}, 0)}, &Sling{queryStructs: []interface{}{paramsA}}, &Sling{queryStructs: []interface{}{paramsA, paramsB}}, @@ -65,8 +65,8 @@ func TestSlingNew(t *testing.T) { } for _, sling := range cases { child := sling.New() - if child.HTTPClient != sling.HTTPClient { - t.Errorf("expected %p, got %p", sling.HTTPClient, child.HTTPClient) + if child.httpClient != sling.httpClient { + t.Errorf("expected %p, got %p", sling.httpClient, child.httpClient) } if child.method != sling.method { t.Errorf("expected %s, got %s", sling.method, child.method) @@ -74,16 +74,16 @@ func TestSlingNew(t *testing.T) { if child.rawURL != sling.rawURL { t.Errorf("expected %s, got %s", sling.rawURL, child.rawURL) } - // Header should be a copy of parent Sling Header. For example, calling + // Header should be a copy of parent Sling header. For example, calling // baseSling.Add("k","v") should not mutate previously created child Slings - if sling.Header != nil { - // struct literal cases don't init Header in usual way, skip Header check - if !reflect.DeepEqual(sling.Header, child.Header) { - t.Errorf("not DeepEqual: expected %v, got %v", sling.Header, child.Header) + if sling.header != nil { + // struct literal cases don't init Header in usual way, skip header check + if !reflect.DeepEqual(sling.header, child.header) { + t.Errorf("not DeepEqual: expected %v, got %v", sling.header, child.header) } - sling.Header.Add("K", "V") - if child.Header.Get("K") != "" { - t.Errorf("child.Header was a reference to original map, should be copy") + sling.header.Add("K", "V") + if child.header.Get("K") != "" { + t.Errorf("child.header was a reference to original map, should be copy") } } // queryStruct slice should be a new slice with a copy of the contents @@ -117,8 +117,8 @@ func TestClientSetter(t *testing.T) { for _, c := range cases { sling := New() sling.Client(c.input) - if sling.HTTPClient != c.expected { - t.Errorf("expected %v, got %v", c.expected, sling.HTTPClient) + if sling.httpClient != c.expected { + t.Errorf("expected %v, got %v", c.expected, sling.httpClient) } } } @@ -199,8 +199,8 @@ func TestAddHeader(t *testing.T) { {New().Add("A", "B").New().Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}}, } for _, c := range cases { - // type conversion from Header to alias'd map for deep equality comparison - headerMap := map[string][]string(c.sling.Header) + // type conversion from header to alias'd map for deep equality comparison + headerMap := map[string][]string(c.sling.header) if !reflect.DeepEqual(c.expectedHeader, headerMap) { t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap) } @@ -221,7 +221,7 @@ func TestSetHeader(t *testing.T) { } for _, c := range cases { // type conversion from Header to alias'd map for deep equality comparison - headerMap := map[string][]string(c.sling.Header) + headerMap := map[string][]string(c.sling.header) if !reflect.DeepEqual(c.expectedHeader, headerMap) { t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap) } @@ -280,10 +280,10 @@ func TestBodyJSONSetter(t *testing.T) { t.Errorf("expected %v, got %v", c.expected, sling.bodyJSON) } // Header Content-Type should be application/json if bodyJSON arg was non-nil - if c.input != nil && sling.Header.Get(contentType) != jsonContentType { - t.Errorf("Incorrect or missing header, expected %s, got %s", jsonContentType, sling.Header.Get(contentType)) - } else if c.input == nil && sling.Header.Get(contentType) != "" { - t.Errorf("did not expect a Content-Type header, got %s", sling.Header.Get(contentType)) + if c.input != nil && sling.header.Get(contentType) != jsonContentType { + t.Errorf("Incorrect or missing header, expected %s, got %s", jsonContentType, sling.header.Get(contentType)) + } else if c.input == nil && sling.header.Get(contentType) != "" { + t.Errorf("did not expect a Content-Type header, got %s", sling.header.Get(contentType)) } } } @@ -309,10 +309,10 @@ func TestBodyFormSetter(t *testing.T) { t.Errorf("expected %v, got %v", c.expected, sling.bodyForm) } // Content-Type should be application/x-www-form-urlencoded if bodyStruct was non-nil - if c.input != nil && sling.Header.Get(contentType) != formContentType { - t.Errorf("Incorrect or missing header, expected %s, got %s", formContentType, sling.Header.Get(contentType)) - } else if c.input == nil && sling.Header.Get(contentType) != "" { - t.Errorf("did not expect a Content-Type header, got %s", sling.Header.Get(contentType)) + if c.input != nil && sling.header.Get(contentType) != formContentType { + t.Errorf("Incorrect or missing header, expected %s, got %s", formContentType, sling.header.Get(contentType)) + } else if c.input == nil && sling.header.Get(contentType) != "" { + t.Errorf("did not expect a Content-Type header, got %s", sling.header.Get(contentType)) } } From 700ae3d45b0068c19cf3adc9e055e49a2fb89aaa Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Fri, 22 May 2015 02:33:21 -0700 Subject: [PATCH 09/11] Improve documentation about Sling extension via New() --- README.md | 51 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5cf56d4..0e1e72b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Sling [![Build Status](https://travis-ci.org/dghubble/sling.png)](https://travis-ci.org/dghubble/sling) [![Coverage](http://gocover.io/_badge/github.com/dghubble/sling)](http://gocover.io/github.com/dghubble/sling) [![GoDoc](http://godoc.org/github.com/dghubble/sling?status.png)](http://godoc.org/github.com/dghubble/sling) -Sling is a Go REST client library for creating and sending API requests. +Sling is a Go HTTP client library for creating and sending API requests. Slings store HTTP Request properties to simplify sending requests and decoding responses. Check [usage](#usage) or the [examples](examples) to learn how to compose a Sling into your API client. @@ -25,31 +25,33 @@ Read [GoDoc](https://godoc.org/github.com/dghubble/sling) ## Usage -Use a simple Sling to set request properties (`Path`, `QueryParams`, etc.) and then create a new `http.Request` by calling `Request()`. +Use a Sling to create an `http.Request` with a chained API for setting properties (path, method, queries, body, etc.). ```go -req, err := sling.New().Get("https://example.com").Request() +type Params struct { + Count int `url:"count,omitempty"` +} + +params := &Params{Count: 5} +req, err := sling.New().Get("https://example.com").QueryStruct(params).Request() client.Do(req) ``` -Slings are much more powerful though. Use them to create REST clients which wrap complex API endpoints. Copy a base Sling with `New()` to avoid repeating common configuration. +### Path -```go -const twitterApi = "https://api.twitter.com/1.1/" -base := sling.New().Base(twitterApi).Client(httpAuthClient) +Use `Path` to set or extend the URL for created Requests. Extension means the path will be resolved relative to the existing URL. -users := base.New().Path("users/") -statuses := base.New().Path("statuses/") +```go +// sends a GET request to http://example.com/foo/bar +req, err := sling.New().Base("http://example.com/").Path("foo/").Path("bar").Request() ``` -Choose an HTTP method, set query parameters, and send the request. +Use `Get`, `Post`, `Put`, `Patch`, `Delete`, or `Head` which are exactly the same as `Path` except they set the HTTP method too. ```go -statuses.New().Get("show.json").QueryStruct(params).Receive(tweet) +req, err := sling.New().Post("http://upload.com/gophers") ``` -The sections below provide more details about setting headers, query parameters, body data, and decoding a typed response. - ### Headers `Add` or `Set` headers which should be applied to all Requests created by a Sling. @@ -131,6 +133,29 @@ req, err := twitterBase.New().Post(path).BodyForm(tweetParams).Request() Requests will include an `application/x-www-form-urlencoded` Content-Type header. +### Extension + +A Sling is effectively a generator for one kind of `http.Request` (say with some path and query params) so setter calls change the result of `Request()`. + +Often, you may wish to create a several kinds of requests, which share some common properties (perhaps a common client and base URL). Each Sling instance provides a `New()` method which creates an independent copy. This allows a parent Sling to be extended to avoid repeating common configuration. + +```go +const twitterApi = "https://api.twitter.com/1.1/" +base := sling.New().Base(twitterApi).Client(httpAuthClient) + +// statuses/show.json Sling +tweetShowSling := base.New().Get("statuses/show.json").QueryStruct(params) +req, err := tweetShowSling.Request() + +// statuses/update.json Sling +tweetPostSling := base.New().Post("statuses/update.json").BodyForm(params) +req, err := tweetPostSling.Request() +``` + +Note that if the calls to `base.New()` were left out, setter calls would mutate the original Sling `base`, which is undesired! We don't intend to send requests to "https://api.twitter.com/1.1/statuses/show.json/statuses/update.json"! + +Recap: If you wish to extend a Sling, create a new child copy with `New()`. + ### Receive Define a JSON struct to decode a type from 2XX success responses. Use `ReceiveSuccess(successV interface{})` to send a new Request and decode the response body into `successV` if it succeeds. From 109c80f0fd4c6d1c606881bb07cd70ff80b5b765 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Fri, 22 May 2015 02:42:55 -0700 Subject: [PATCH 10/11] Fix default Request method to be "GET" instead of "" --- sling.go | 1 + sling_test.go | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/sling.go b/sling.go index 2196409..7dc32e6 100644 --- a/sling.go +++ b/sling.go @@ -38,6 +38,7 @@ type Sling struct { func New() *Sling { return &Sling{ httpClient: http.DefaultClient, + method: "GET", header: make(http.Header), queryStructs: make([]interface{}, 0), } diff --git a/sling_test.go b/sling_test.go index c8d31a8..835cad5 100644 --- a/sling_test.go +++ b/sling_test.go @@ -170,6 +170,7 @@ func TestMethodSetters(t *testing.T) { sling *Sling expectedMethod string }{ + {New().Path("http://a.io"), "GET"}, {New().Head("http://a.io"), "HEAD"}, {New().Get("http://a.io"), "GET"}, {New().Post("http://a.io"), "POST"}, @@ -325,23 +326,23 @@ func TestRequest_urlAndMethod(t *testing.T) { expectedURL string expectedErr error }{ - {New().Base("http://a.io"), "", "http://a.io", nil}, - {New().Path("http://a.io"), "", "http://a.io", nil}, + {New().Base("http://a.io"), "GET", "http://a.io", nil}, + {New().Path("http://a.io"), "GET", "http://a.io", nil}, {New().Get("http://a.io"), "GET", "http://a.io", nil}, {New().Put("http://a.io"), "PUT", "http://a.io", nil}, - {New().Base("http://a.io/").Path("foo"), "", "http://a.io/foo", nil}, + {New().Base("http://a.io/").Path("foo"), "GET", "http://a.io/foo", nil}, {New().Base("http://a.io/").Post("foo"), "POST", "http://a.io/foo", nil}, // if relative path is an absolute url, base is ignored - {New().Base("http://a.io").Path("http://b.io"), "", "http://b.io", nil}, - {New().Path("http://a.io").Path("http://b.io"), "", "http://b.io", nil}, + {New().Base("http://a.io").Path("http://b.io"), "GET", "http://b.io", nil}, + {New().Path("http://a.io").Path("http://b.io"), "GET", "http://b.io", nil}, // last method setter takes priority {New().Get("http://b.io").Post("http://a.io"), "POST", "http://a.io", nil}, {New().Post("http://a.io/").Put("foo/").Delete("bar"), "DELETE", "http://a.io/foo/bar", nil}, // last Base setter takes priority - {New().Base("http://a.io").Base("http://b.io"), "", "http://b.io", nil}, + {New().Base("http://a.io").Base("http://b.io"), "GET", "http://b.io", nil}, // Path setters are additive - {New().Base("http://a.io/").Path("foo/").Path("bar"), "", "http://a.io/foo/bar", nil}, - {New().Path("http://a.io/").Path("foo/").Path("bar"), "", "http://a.io/foo/bar", nil}, + {New().Base("http://a.io/").Path("foo/").Path("bar"), "GET", "http://a.io/foo/bar", nil}, + {New().Path("http://a.io/").Path("foo/").Path("bar"), "GET", "http://a.io/foo/bar", nil}, // removes extra '/' between base and ref url {New().Base("http://a.io/").Get("/foo"), "GET", "http://a.io/foo", nil}, } From a2c371868dbd266164e53f1a52c837902b0961e0 Mon Sep 17 00:00:00 2001 From: Dalton Hubble Date: Sat, 23 May 2015 16:03:16 -0700 Subject: [PATCH 11/11] Doc improvements and fixes --- CHANGES.md | 4 +- README.md | 20 +++++-- doc.go | 168 ++++++++++++++++++++++++++++++++++++++++++++++++----- sling.go | 8 +-- 4 files changed, 173 insertions(+), 27 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d8fda07..7b50b17 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,12 +2,13 @@ Notable changes between releases. -## v1.0.0-rc (unreleased) +## v1.0.0 (2015-05-23) * Added support for receiving and decoding error JSON structs * Renamed Sling `JsonBody` setter to `BodyJSON` (breaking) * Renamed Sling `BodyStruct` setter to `BodyForm` (breaking) * Renamed Sling fields `httpClient`, `method`, `rawURL`, and `header` to be internal (breaking) +* Changed `Do` and `Receive` to skip response JSON decoding if "application/json" Content-Type is missing * Changed `Sling.Receive(v interface{})` to `Sling.Receive(successV, failureV interface{})` (breaking) * Previously `Receive` attempted to decode the response Body in all cases * Updated `Receive` will decode the response Body into successV for 2XX responses or decode the Body into failureV for other status codes. Pass a nil `successV` or `failureV` to skip JSON decoding into that value. @@ -15,7 +16,6 @@ Notable changes between releases. * To retain the old behavior, duplicate the first argument (e.g. s.Receive(&tweet, &tweet)) * Changed `Sling.Do(http.Request, v interface{})` to `Sling.Do(http.Request, successV, failureV interface{})` (breaking) * See the changelog entry about `Receive`, the upgrade path is the same. -* Require responses to have Content-Type containing application/json before attempting JSON decoding in `Recieve`, `ReceiveSuccess` or `Do`. * Removed HEAD, GET, POST, PUT, PATCH, DELETE constants, no reason to export them (breaking) ## v0.4.0 (2015-04-26) diff --git a/README.md b/README.md index 0e1e72b..631c69e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Sling is a Go HTTP client library for creating and sending API requests. Slings store HTTP Request properties to simplify sending requests and decoding responses. Check [usage](#usage) or the [examples](examples) to learn how to compose a Sling into your API client. +Note: Sling **v1.0** recently introduced some breaking changes. See [changes](CHANGES.md). + ### Features * Base/Path - path extend a Sling for different endpoints @@ -31,8 +33,8 @@ Use a Sling to create an `http.Request` with a chained API for setting propertie type Params struct { Count int `url:"count,omitempty"` } - params := &Params{Count: 5} + req, err := sling.New().Get("https://example.com").QueryStruct(params).Request() client.Do(req) ``` @@ -54,7 +56,7 @@ req, err := sling.New().Post("http://upload.com/gophers") ### Headers -`Add` or `Set` headers which should be applied to all Requests created by a Sling. +`Add` or `Set` headers which should be applied to the Requests created by a Sling. ```go base := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client") @@ -133,11 +135,14 @@ req, err := twitterBase.New().Post(path).BodyForm(tweetParams).Request() Requests will include an `application/x-www-form-urlencoded` Content-Type header. -### Extension +### Extend a -A Sling is effectively a generator for one kind of `http.Request` (say with some path and query params) so setter calls change the result of `Request()`. +Each distinct Sling generates an `http.Request` (say with some path and query +params) each time `Request()` is called, based on its state. When creating +different kinds of requests using distinct Slings, you may wish to extend +an existing Sling to minimize duplication (e.g. a common client). -Often, you may wish to create a several kinds of requests, which share some common properties (perhaps a common client and base URL). Each Sling instance provides a `New()` method which creates an independent copy. This allows a parent Sling to be extended to avoid repeating common configuration. +Each Sling instance provides a `New()` method which creates an independent copy, so setting properties on the child won't mutate the parent Sling. ```go const twitterApi = "https://api.twitter.com/1.1/" @@ -152,7 +157,10 @@ tweetPostSling := base.New().Post("statuses/update.json").BodyForm(params) req, err := tweetPostSling.Request() ``` -Note that if the calls to `base.New()` were left out, setter calls would mutate the original Sling `base`, which is undesired! We don't intend to send requests to "https://api.twitter.com/1.1/statuses/show.json/statuses/update.json"! +Without the calls to `base.New()`, tweetShowSling and tweetPostSling reference +the base Sling and POST to +"https://api.twitter.com/1.1/statuses/show.json/statuses/update.json", which +is undesired. Recap: If you wish to extend a Sling, create a new child copy with `New()`. diff --git a/doc.go b/doc.go index ef9505a..aa08e6e 100644 --- a/doc.go +++ b/doc.go @@ -1,31 +1,169 @@ /* -Package sling is a Go REST client library for creating and sending requests. +Package sling is a Go HTTP client library for creating and sending API requests. -Slings store http Request properties to simplify sending requests and decoding +Slings store HTTP Request properties to simplify sending requests and decoding responses. Check the examples to learn how to compose a Sling into your API client. -Use a simple Sling to set request properties (Path, QueryParams, etc.) and -then create a new http.Request by calling Request(). +Usage - req, err := sling.New().Get("https://example.com").Request() +Use a Sling to create an http.Request with a chained API for setting properties +(path, method, queries, body, etc.). + + type Params struct { + Count int `url:"count,omitempty"` + } + params := &Params{Count: 5} + + req, err := sling.New().Get("https://example.com").QueryStruct(params).Request() client.Do(req) -Slings are much more powerful though. Use them to create REST clients -which wrap complex API endpoints. Copy a base Sling with New() to avoid -repeating common configuration. +Path + +Use Path to set or extend the URL for created Requests. Extension means the +path will be resolved relative to the existing URL. + + // sends a GET request to http://example.com/foo/bar + req, err := sling.New().Base("http://example.com/").Path("foo/").Path("bar").Request() + +Use Get, Post, Put, Patch, Delete, or Head which are exactly the same as Path +except they set the HTTP method too. + + req, err := sling.New().Post("http://upload.com/gophers") + +Headers + +Add or Set headers which should be applied to the Requests created by a Sling. + + base := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client") + req, err := base.New().Get("gophergram/list").Request() + +QueryStruct + +Define url parameter structs (https://godoc.org/github.com/google/go-querystring/query) +and use QueryStruct to encode query parameters. + + // Github Issue Parameters + type IssueParams struct { + Filter string `url:"filter,omitempty"` + State string `url:"state,omitempty"` + Labels string `url:"labels,omitempty"` + Sort string `url:"sort,omitempty"` + Direction string `url:"direction,omitempty"` + Since string `url:"since,omitempty"` + } + + githubBase := sling.New().Base("https://api.github.com/").Client(httpClient) + path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) + + params := &IssueParams{Sort: "updated", State: "open"} + req, err := githubBase.New().Get(path).QueryStruct(params).Request() + +Json Body + +Make a Sling include JSON in the Body of its Requests using BodyJSON. + + type IssueRequest struct { + Title string `json:"title,omitempty"` + Body string `json:"body,omitempty"` + Assignee string `json:"assignee,omitempty"` + Milestone int `json:"milestone,omitempty"` + Labels []string `json:"labels,omitempty"` + } + + githubBase := sling.New().Base("https://api.github.com/").Client(httpClient) + path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) - const twitterApi = "https://https://api.twitter.com/1.1/" + body := &IssueRequest{ + Title: "Test title", + Body: "Some issue", + } + req, err := githubBase.New().Post(path).BodyJSON(body).Request() + +Requests will include an "application/json" Content-Type header. + +Form Body + +Make a Sling include a url-tagged struct as a url-encoded form in the Body of +its Requests using BodyForm. + + type StatusUpdateParams struct { + Status string `url:"status,omitempty"` + InReplyToStatusId int64 `url:"in_reply_to_status_id,omitempty"` + MediaIds []int64 `url:"media_ids,omitempty,comma"` + } + + tweetParams := &StatusUpdateParams{Status: "writing some Go"} + req, err := twitterBase.New().Post(path).BodyForm(tweetParams).Request() + +Requests will include an "application/x-www-form-urlencoded" Content-Type +header. + +Extend a Sling + +Each distinct Sling generates an http.Request (say with some path and query +params) each time Request() is called, based on its state. When creating +different kinds of requests using distinct Slings, you may wish to extend +an existing Sling to minimize duplication (e.g. a common client). + +Each Sling instance provides a New() method which creates an independent copy, +so setting properties on the child won't mutate the parent Sling. + + const twitterApi = "https://api.twitter.com/1.1/" base := sling.New().Base(twitterApi).Client(httpAuthClient) - users := base.New().Path("users/") - statuses := base.New().Path("statuses/") + // statuses/show.json Sling + tweetShowSling := base.New().Get("statuses/show.json").QueryStruct(params) + req, err := tweetShowSling.Request() + + // statuses/update.json Sling + tweetPostSling := base.New().Post("statuses/update.json").BodyForm(params) + req, err := tweetPostSling.Request() + +Without the calls to base.New(), tweetShowSling and tweetPostSling reference +the base Sling and POST to +"https://api.twitter.com/1.1/statuses/show.json/statuses/update.json", which +is undesired. + +Recap: If you wish to extend a Sling, create a new child copy with New(). + +Receive + +Define a JSON struct to decode a type from 2XX success responses. Use +ReceiveSuccess(successV interface{}) to send a new Request and decode the +response body into successV if it succeeds. + + // Github Issue (abbreviated) + type Issue struct { + Title string `json:"title"` + Body string `json:"body"` + } + + issues := new([]Issue) + resp, err := githubBase.New().Get(path).QueryStruct(params).ReceiveSuccess(issues) + fmt.Println(issues, resp, err) + +Most APIs return failure responses with JSON error details. To decode these, +define success and failure JSON structs. Use +Receive(successV, failureV interface{}) to send a new Request that will +automatically decode the response into the successV for 2XX responses or into +failureV for non-2XX responses. -Choose an http method, set query parameters, and send the request. + type GithubError struct { + Message string `json:"message"` + Errors []struct { + Resource string `json:"resource"` + Field string `json:"field"` + Code string `json:"code"` + } `json:"errors"` + DocumentationURL string `json:"documentation_url"` + } - statuses.New().Get("show.json").QueryStruct(params).Receive(tweet) + issues := new([]Issue) + githubError := new(GithubError) + resp, err := githubBase.New().Get(path).QueryStruct(params).Receive(issues, githubError) + fmt.Println(issues, githubError, resp, err) -The usage README provides more details about setting headers, query parameters, -body data, and decoding a typed response after sending. +Pass a nil successV or failureV argument to skip JSON decoding into that value. */ package sling diff --git a/sling.go b/sling.go index 7dc32e6..3079493 100644 --- a/sling.go +++ b/sling.go @@ -45,11 +45,11 @@ func New() *Sling { } // New returns a copy of a Sling for creating a new Sling with properties -// from a base Sling. For example, +// from a parent Sling. For example, // -// baseSling := sling.New().Client(client).Base("https://api.io/") -// fooSling := baseSling.New().Get("foo/") -// barSling := baseSling.New().Get("bar/") +// parentSling := sling.New().Client(client).Base("https://api.io/") +// fooSling := parentSling.New().Get("foo/") +// barSling := parentSling.New().Get("bar/") // // fooSling and barSling will both use the same client, but send requests to // https://api.io/foo/ and https://api.io/bar/ respectively.