pax_global_header00006660000000000000000000000064130254667310014521gustar00rootroot0000000000000052 comment=eb56e89ac5088bebb12eef3cb4b293300f43608b sling-1.1.0/000077500000000000000000000000001302546673100126345ustar00rootroot00000000000000sling-1.1.0/.travis.yml000066400000000000000000000002001302546673100147350ustar00rootroot00000000000000language: go go: - 1.6 - 1.7 - tip install: - go get github.com/golang/lint/golint - go get -v -t . script: - ./testsling-1.1.0/CHANGES.md000066400000000000000000000051171302546673100142320ustar00rootroot00000000000000# Sling Changelog Notable changes between releases. ## Latest ## v1.1.0 (2016-12-19) * Allow JSON decoding, regardless of response Content-Type (#26) * Add `BodyProvider` interface and setter so request Body encoding can be customized (#23) * Add `Doer` interface and setter so request sending behavior can be customized (#21) * Add `SetBasicAuth` setter for Authorization headers (#16) * Add Sling `Body` setter to set an `io.Reader` on the Request (#9) ## 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. * 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. sling-1.1.0/LICENSE000066400000000000000000000020671302546673100136460ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Dalton Hubble Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.sling-1.1.0/README.md000066400000000000000000000216701302546673100141210ustar00rootroot00000000000000 # Sling [![Build Status](https://travis-ci.org/dghubble/sling.png?branch=master)](https://travis-ci.org/dghubble/sling) [![GoDoc](https://godoc.org/github.com/dghubble/sling?status.png)](https://godoc.org/github.com/dghubble/sling) 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. ### Features * Method Setters: Get/Post/Put/Patch/Delete/Head * Add or Set Request Headers * Base/Path: Extend a Sling for different endpoints * Encode structs into URL query parameters * Encode a form or JSON into the Request Body * Receive JSON success or failure responses ## Install go get github.com/dghubble/sling ## Documentation Read [GoDoc](https://godoc.org/github.com/dghubble/sling) ## Usage Use a Sling to set path, method, header, query, or body properties and create an `http.Request`. ```go 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) ``` ### Path Use `Path` to set or extend the URL for created Requests. Extension means the path will be resolved relative to the existing URL. ```go // creates a GET request to https://example.com/foo/bar req, err := sling.New().Base("https://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. ```go req, err := sling.New().Post("http://upload.com/gophers") ``` ### Headers `Add` or `Set` headers for requests created by a Sling. ```go s := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client") req, err := s.New().Get("gophergram/list").Request() ``` ### Query #### QueryStruct Define [url tagged structs](https://godoc.org/github.com/google/go-querystring/query). Use `QueryStruct` to encode a struct as query parameters on requests. ```go // 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"` } ``` ```go 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() ``` ### Body #### JSON Body Define [JSON tagged structs](https://golang.org/pkg/encoding/json/). Use `BodyJSON` to JSON encode a struct as the Body on requests. ```go 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"` } ``` ```go githubBase := sling.New().Base("https://api.github.com/").Client(httpClient) path := fmt.Sprintf("repos/%s/%s/issues", owner, repo) 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 Define [url tagged structs](https://godoc.org/github.com/google/go-querystring/query). Use `BodyForm` to form url encode a struct as the Body on requests. ```go type StatusUpdateParams struct { Status string `url:"status,omitempty"` InReplyToStatusId int64 `url:"in_reply_to_status_id,omitempty"` MediaIds []int64 `url:"media_ids,omitempty,comma"` } ``` ```go 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. #### Plain Body Use `Body` to set a plain `io.Reader` on requests created by a Sling. ```go body := strings.NewReader("raw body") req, err := sling.New().Base("https://example.com").Body(body).Request() ``` Set a content type header, if desired (e.g. `Set("Content-Type", "text/plain")`). ### Extend a Sling Each Sling creates a standard `http.Request` (e.g. with some path and query params) each time `Request()` is called. You may wish to extend an existing Sling to minimize duplication (e.g. a common client or base url). 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/" base := sling.New().Base(twitterApi).Client(authClient) // 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` would 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()`. ### Sending #### 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. ```go // Github Issue (abbreviated) type Issue struct { Title string `json:"title"` Body string `json:"body"` } ``` ```go 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. ```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 [lists](https://developer.github.com/v3/issues/#list-issues-for-a-repository) repository issues. ```go const baseURL = "https://api.github.com/" type IssueService struct { sling *sling.Sling } func NewIssueService(httpClient *http.Client) *IssueService { return &IssueService{ sling: sling.New().Client(httpClient).Base(baseURL), } } 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 { err = githubError } return *issues, resp, err } ``` ## Example APIs using Sling * Digits [dghubble/go-digits](https://github.com/dghubble/go-digits) * GoSquared [drinkin/go-gosquared](https://github.com/drinkin/go-gosquared) * Kala [ajvb/kala](https://github.com/ajvb/kala) * Parse [fergstar/go-parse](https://github.com/fergstar/go-parse) * Rdio [apriendeau/shares](https://github.com/apriendeau/shares) * Swagger Generator [swagger-api/swagger-codegen](https://github.com/swagger-api/swagger-codegen) * Twitter [dghubble/go-twitter](https://github.com/dghubble/go-twitter) * Hacker News [mirceamironenco/go-hackernews](https://github.com/mirceamironenco/go-hackernews) * Stacksmith [jesustinoco/go-smith](https://github.com/jesustinoco/go-smith) Create a Pull Request to add a link to your own API. ## 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. This project borrows and abstracts those ideas into a Sling, an agnostic component any API client can use for creating and sending requests. ## Contributing See the [Contributing Guide](https://gist.github.com/dghubble/be682c123727f70bcfe7). ## License [MIT License](LICENSE) sling-1.1.0/body.go000066400000000000000000000030071302546673100141200ustar00rootroot00000000000000package sling import ( "bytes" "encoding/json" "io" "strings" goquery "github.com/google/go-querystring/query" ) // BodyProvider provides Body content for http.Request attachment. type BodyProvider interface { // ContentType returns the Content-Type of the body. ContentType() string // Body returns the io.Reader body. Body() (io.Reader, error) } // bodyProvider provides the wrapped body value as a Body for reqests. type bodyProvider struct { body io.Reader } func (p bodyProvider) ContentType() string { return "" } func (p bodyProvider) Body() (io.Reader, error) { return p.body, nil } // jsonBodyProvider encodes a JSON tagged struct value as a Body for requests. // See https://golang.org/pkg/encoding/json/#MarshalIndent for details. type jsonBodyProvider struct { payload interface{} } func (p jsonBodyProvider) ContentType() string { return jsonContentType } func (p jsonBodyProvider) Body() (io.Reader, error) { buf := &bytes.Buffer{} err := json.NewEncoder(buf).Encode(p.payload) if err != nil { return nil, err } return buf, nil } // formBodyProvider encodes a url tagged struct value as Body for requests. // See https://godoc.org/github.com/google/go-querystring/query for details. type formBodyProvider struct { payload interface{} } func (p formBodyProvider) ContentType() string { return formContentType } func (p formBodyProvider) Body() (io.Reader, error) { values, err := goquery.Values(p.payload) if err != nil { return nil, err } return strings.NewReader(values.Encode()), nil } sling-1.1.0/doc.go000066400000000000000000000136701302546673100137370ustar00rootroot00000000000000/* 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 responses. Check the examples to learn how to compose a Sling into your API client. Usage Use a Sling to set path, method, header, query, or body properties and create an http.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) Path Use Path to set or extend the URL for created Requests. Extension means the path will be resolved relative to the existing URL. // creates a GET request to https://example.com/foo/bar req, err := sling.New().Base("https://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 for requests created by a Sling. s := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client") req, err := s.New().Get("gophergram/list").Request() QueryStruct Define url parameter structs (https://godoc.org/github.com/google/go-querystring/query). Use QueryStruct to encode a struct as query parameters on requests. // 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 Define JSON tagged structs (https://golang.org/pkg/encoding/json/). Use BodyJSON to JSON encode a struct as the Body on requests. 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) 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 Define url tagged structs (https://godoc.org/github.com/google/go-querystring/query). Use BodyForm to form url encode a struct as the Body on requests. 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. Plain Body Use Body to set a plain io.Reader on requests created by a Sling. body := strings.NewReader("raw body") req, err := sling.New().Base("https://example.com").Body(body).Request() Set a content type header, if desired (e.g. Set("Content-Type", "text/plain")). Extend a Sling Each Sling generates an http.Request (say with some path and query params) each time Request() is called, based on its state. When creating different 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(authClient) // 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 would 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. 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"` } 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. */ package sling sling-1.1.0/examples/000077500000000000000000000000001302546673100144525ustar00rootroot00000000000000sling-1.1.0/examples/README.md000066400000000000000000000010401302546673100157240ustar00rootroot00000000000000 ## Example API Client with Sling Try the example Github API Client. cd examples go get . List the public issues on the [github.com/golang/go](https://github.com/golang/go) repository. go run github.go To list your public and private Github issues, pass your [Github Access Token](https://github.com/settings/tokens) go run github.go -access-token=xxx or set the `GITHUB_ACCESS_TOKEN` environment variable. For a complete Github API, see the excellent [google/go-github](https://github.com/google/go-github) package.sling-1.1.0/examples/github.go000066400000000000000000000110651302546673100162660ustar00rootroot00000000000000package main import ( "flag" "fmt" "log" "net/http" "os" "github.com/coreos/pkg/flagutil" "github.com/dghubble/sling" "golang.org/x/oauth2" ) const baseURL = "https://api.github.com/" // Issue is a simplified Github issue // https://developer.github.com/v3/issues/#response 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"` } // 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"` Assignee string `json:"assignee,omitempty"` Milestone int `json:"milestone,omitempty"` 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"` 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"` } // 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), } } // 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 { err = 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 { err = 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).BodyJSON(issueBody).Receive(issue, githubError) if err == nil { err = githubError } return issue, resp, err } // Client to wrap services // 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), } } func main() { // Github Unauthenticated API client := NewClient(nil) params := &IssueListParams{Sort: "updated"} issues, _, _ := client.IssueService.ListByRepo("golang", "go", params) fmt.Printf("Public golang/go Issues:\n%v\n", issues) // Github OAuth2 API flags := flag.NewFlagSet("github-example", flag.ExitOnError) // -access-token=xxx or GITHUB_ACCESS_TOKEN env var accessToken := flags.String("access-token", "", "Github Access Token") flags.Parse(os.Args[1:]) flagutil.SetFlagsFromEnv(flags, "GITHUB") if *accessToken == "" { log.Fatal("Github Access Token required to list private issues") } config := &oauth2.Config{} token := &oauth2.Token{AccessToken: *accessToken} httpClient := config.Client(oauth2.NoContext, token) client = NewClient(httpClient) issues, _, _ = client.IssueService.List(params) fmt.Printf("Your Github Issues:\n%v\n", issues) // body := &IssueRequest{ // Title: "Test title", // Body: "Some test issue", // } // issue, _, _ := client.IssueService.Create("dghubble", "temp", body) // fmt.Println(issue) } sling-1.1.0/sling.go000066400000000000000000000264171302546673100143110ustar00rootroot00000000000000package sling import ( "encoding/base64" "encoding/json" "io" "net/http" "net/url" goquery "github.com/google/go-querystring/query" ) const ( contentType = "Content-Type" jsonContentType = "application/json" formContentType = "application/x-www-form-urlencoded" ) // Doer executes http requests. It is implemented by *http.Client. You can // wrap *http.Client with layers of Doers to form a stack of client-side // middleware. type Doer interface { Do(req *http.Request) (*http.Response, error) } // Sling is an HTTP Request builder and sender. type Sling struct { // http Client for doing requests httpClient Doer // 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 // url tagged query structs queryStructs []interface{} // body provider bodyProvider BodyProvider } // New returns a new Sling with an http DefaultClient. func New() *Sling { return &Sling{ httpClient: http.DefaultClient, method: "GET", header: make(http.Header), queryStructs: make([]interface{}, 0), } } // New returns a copy of a Sling for creating a new Sling with properties // from a parent Sling. For example, // // 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. // // 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) for k, v := range s.header { headerCopy[k] = v } return &Sling{ httpClient: s.httpClient, method: s.method, rawURL: s.rawURL, header: headerCopy, queryStructs: append([]interface{}{}, s.queryStructs...), bodyProvider: s.bodyProvider, } } // Http Client // Client sets the http Client used to do requests. If a nil client is given, // the http.DefaultClient will be used. func (s *Sling) Client(httpClient *http.Client) *Sling { if httpClient == nil { return s.Doer(http.DefaultClient) } return s.Doer(httpClient) } // Doer sets the custom Doer implementation used to do requests. // If a nil client is given, the http.DefaultClient will be used. func (s *Sling) Doer(doer Doer) *Sling { if doer == nil { s.httpClient = http.DefaultClient } else { s.httpClient = doer } return s } // Method // Head sets the Sling method to HEAD and sets the given pathURL. func (s *Sling) Head(pathURL string) *Sling { 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" 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" 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" 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" 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" return s.Path(pathURL) } // Header // 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) 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) return s } // SetBasicAuth sets the Authorization header to use HTTP Basic Authentication // with the provided username and password. With HTTP Basic Authentication // the provided username and password are not encrypted. func (s *Sling) SetBasicAuth(username, password string) *Sling { return s.Set("Authorization", "Basic "+basicAuth(username, password)) } // basicAuth returns the base64 encoded username:password for basic auth copied // from net/http. func basicAuth(username, password string) string { auth := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(auth)) } // Url // 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 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. func (s *Sling) Path(path string) *Sling { baseURL, baseErr := url.Parse(s.rawURL) pathURL, pathErr := url.Parse(path) if baseErr == nil && pathErr == nil { s.rawURL = baseURL.ResolveReference(pathURL).String() return s } return s } // QueryStruct appends the queryStruct to the Sling's queryStructs. The value // pointed to by each queryStruct will be encoded as url query parameters on // new requests (see Request()). // The queryStruct 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) QueryStruct(queryStruct interface{}) *Sling { if queryStruct != nil { s.queryStructs = append(s.queryStructs, queryStruct) } return s } // Body // Body sets the Sling's body. The body value will be set as the Body on new // requests (see Request()). // If the provided body is also an io.Closer, the request Body will be closed // by http.Client methods. func (s *Sling) Body(body io.Reader) *Sling { if body == nil { return s } return s.BodyProvider(bodyProvider{body: body}) } // BodyProvider sets the Sling's body provider. func (s *Sling) BodyProvider(body BodyProvider) *Sling { if body == nil { return s } s.bodyProvider = body ct := body.ContentType() if ct != "" { s.Set(contentType, ct) } return s } // BodyJSON sets the Sling's bodyJSON. The value pointed to by the bodyJSON // will be JSON encoded as the Body on new requests (see Request()). // 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) BodyJSON(bodyJSON interface{}) *Sling { if bodyJSON == nil { return s } return s.BodyProvider(jsonBodyProvider{payload: bodyJSON}) } // BodyForm sets the Sling's bodyForm. The value pointed to by the bodyForm // will be url encoded as the Body on new requests (see Request()). // The bodyForm 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) BodyForm(bodyForm interface{}) *Sling { if bodyForm == nil { return s } return s.BodyProvider(formBodyProvider{payload: bodyForm}) } // Requests // Request returns a new http.Request created with the Sling properties. // 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) if err != nil { return nil, err } err = addQueryStructs(reqURL, s.queryStructs) if err != nil { return nil, err } var body io.Reader if s.bodyProvider != nil { body, err = s.bodyProvider.Body() if err != nil { return nil, err } } req, err := http.NewRequest(s.method, reqURL.String(), body) if err != nil { return nil, err } addHeaders(req, s.header) return req, err } // addQueryStructs parses url tagged query structs using go-querystring to // encode them to url.Values and format them onto the url.RawQuery. Any // query parsing or encoding errors are returned. func addQueryStructs(reqURL *url.URL, queryStructs []interface{}) error { urlValues, err := url.ParseQuery(reqURL.RawQuery) if err != nil { return err } // encodes query structs into a url.Values map and merges maps for _, queryStruct := range queryStructs { queryValues, err := goquery.Values(queryStruct) if err != nil { return err } for key, values := range queryValues { for _, value := range values { urlValues.Add(key, value) } } } // url.Values format to a sorted "url encoded" string, e.g. "key=val&foo=bar" reqURL.RawQuery = urlValues.Encode() return nil } // addHeaders adds the key, value pairs from the given http.Header to the // request. Values for existing keys are appended to the keys values. func addHeaders(req *http.Request, header http.Header) { for key, values := range header { for _, value := range values { req.Header.Add(key, value) } } } // Sending // 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, successV, failureV) } // 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 successV != nil || failureV != nil { err = decodeResponseJSON(resp, successV, failureV) } return resp, err } // 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 } // 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 { return json.NewDecoder(resp.Body).Decode(v) } sling-1.1.0/sling_test.go000066400000000000000000000721031302546673100153410ustar00rootroot00000000000000package sling import ( "bytes" "errors" "fmt" "io" "io/ioutil" "math" "net/http" "net/http/httptest" "net/url" "reflect" "strings" "testing" ) type FakeParams struct { KindName string `url:"kind_name"` Count int `url:"count"` } // Url-tagged query struct var paramsA = struct { Limit int `url:"limit"` }{ 30, } var paramsB = FakeParams{KindName: "recent", Count: 25} // Json-tagged model struct type FakeModel struct { Text string `json:"text,omitempty"` FavoriteCount int64 `json:"favorite_count,omitempty"` Temperature float64 `json:"temperature,omitempty"` } 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.header == nil { t.Errorf("Header map not initialized with make") } if sling.queryStructs == nil { t.Errorf("queryStructs not initialized with make") } } func TestSlingNew(t *testing.T) { fakeBodyProvider := jsonBodyProvider{FakeModel{}} cases := []*Sling{ &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{bodyProvider: fakeBodyProvider}, &Sling{bodyProvider: fakeBodyProvider}, &Sling{bodyProvider: nil}, New().Add("Content-Type", "application/json"), New().Add("A", "B").Add("a", "c").New(), New().Add("A", "B").New().Add("a", "c"), New().BodyForm(paramsB), New().BodyForm(paramsB).New(), } for _, sling := range cases { child := sling.New() if child.httpClient != sling.httpClient { t.Errorf("expected %v, got %v", sling.httpClient, child.httpClient) } 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) } // 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) } 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 if len(sling.queryStructs) > 0 { // mutating one slice should not mutate the other child.queryStructs[0] = nil if sling.queryStructs[0] == nil { t.Errorf("child.queryStructs was a re-slice, expected slice with copied contents") } } // body should be copied if child.bodyProvider != sling.bodyProvider { t.Errorf("expected %v, got %v", sling.bodyProvider, child.bodyProvider) } } } func TestClientSetter(t *testing.T) { developerClient := &http.Client{} cases := []struct { input *http.Client expected *http.Client }{ {nil, http.DefaultClient}, {developerClient, developerClient}, } for _, c := range cases { sling := New() sling.Client(c.input) if sling.httpClient != c.expected { t.Errorf("input %v, expected %v, got %v", c.input, c.expected, sling.httpClient) } } } func TestDoerSetter(t *testing.T) { developerClient := &http.Client{} cases := []struct { input Doer expected Doer }{ {nil, http.DefaultClient}, {developerClient, developerClient}, } for _, c := range cases { sling := New() sling.Doer(c.input) if sling.httpClient != c.expected { t.Errorf("input %v, expected %v, got %v", c.input, c.expected, sling.httpClient) } } } 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) } } } func TestPathSetter(t *testing.T) { cases := []struct { rawURL string path string expectedRawURL string }{ {"http://a.io/", "foo", "http://a.io/foo"}, {"http://a.io/", "/foo", "http://a.io/foo"}, {"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 {"http://a.io/foo", "bar", "http://a.io/bar"}, {"http://a.io/foo", "/bar", "http://a.io/bar"}, // path extension is absolute {"http://a.io", "http://b.io/", "http://b.io/"}, {"http://a.io/", "http://b.io/", "http://b.io/"}, {"http://a.io", "http://b.io", "http://b.io"}, {"http://a.io/", "http://b.io", "http://b.io"}, // empty base, empty path {"", "http://b.io", "http://b.io"}, {"http://a.io", "", "http://a.io"}, {"", "", ""}, } 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) } } } func TestMethodSetters(t *testing.T) { cases := []struct { 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"}, {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) } } } func TestAddHeader(t *testing.T) { cases := []struct { sling *Sling expectedHeader map[string][]string }{ {New().Add("authorization", "OAuth key=\"value\""), map[string][]string{"Authorization": []string{"OAuth key=\"value\""}}}, // header keys should be canonicalized {New().Add("content-tYPE", "application/json").Add("User-AGENT", "sling"), map[string][]string{"Content-Type": []string{"application/json"}, "User-Agent": []string{"sling"}}}, // values for existing keys should be appended {New().Add("A", "B").Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}}, // Add should add to values for keys added by parent Slings {New().Add("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}}, {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) if !reflect.DeepEqual(c.expectedHeader, headerMap) { t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap) } } } func TestSetHeader(t *testing.T) { cases := []struct { sling *Sling expectedHeader map[string][]string }{ // should replace existing values associated with key {New().Add("A", "B").Set("a", "c"), map[string][]string{"A": []string{"c"}}}, {New().Set("content-type", "A").Set("Content-Type", "B"), map[string][]string{"Content-Type": []string{"B"}}}, // Set should replace values received by copying parent Slings {New().Set("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}}, {New().Add("A", "B").New().Set("a", "c"), map[string][]string{"A": []string{"c"}}}, } for _, c := range cases { // 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) } } } func TestBasicAuth(t *testing.T) { cases := []struct { sling *Sling expectedAuth []string }{ // basic auth: username & password {New().SetBasicAuth("Aladdin", "open sesame"), []string{"Aladdin", "open sesame"}}, // empty username {New().SetBasicAuth("", "secret"), []string{"", "secret"}}, // empty password {New().SetBasicAuth("admin", ""), []string{"admin", ""}}, } for _, c := range cases { req, err := c.sling.Request() if err != nil { t.Errorf("unexpected error when building Request with .SetBasicAuth()") } username, password, ok := req.BasicAuth() if !ok { t.Errorf("basic auth missing when expected") } auth := []string{username, password} if !reflect.DeepEqual(c.expectedAuth, auth) { t.Errorf("not DeepEqual: expected %v, got %v", c.expectedAuth, auth) } } } func TestQueryStructSetter(t *testing.T) { cases := []struct { sling *Sling expectedStructs []interface{} }{ {New(), []interface{}{}}, {New().QueryStruct(nil), []interface{}{}}, {New().QueryStruct(paramsA), []interface{}{paramsA}}, {New().QueryStruct(paramsA).QueryStruct(paramsA), []interface{}{paramsA, paramsA}}, {New().QueryStruct(paramsA).QueryStruct(paramsB), []interface{}{paramsA, paramsB}}, {New().QueryStruct(paramsA).New(), []interface{}{paramsA}}, {New().QueryStruct(paramsA).New().QueryStruct(paramsB), []interface{}{paramsA, paramsB}}, } for _, c := range cases { if count := len(c.sling.queryStructs); count != len(c.expectedStructs) { t.Errorf("expected length %d, got %d", len(c.expectedStructs), count) } check: for _, expected := range c.expectedStructs { for _, param := range c.sling.queryStructs { if param == expected { continue check } } t.Errorf("expected to find %v in %v", expected, c.sling.queryStructs) } } } func TestBodyJSONSetter(t *testing.T) { fakeModel := &FakeModel{} fakeBodyProvider := jsonBodyProvider{payload: fakeModel} cases := []struct { initial BodyProvider input interface{} expected BodyProvider }{ // json tagged struct is set as bodyJSON {nil, fakeModel, fakeBodyProvider}, // nil argument to bodyJSON does not replace existing bodyJSON {fakeBodyProvider, nil, fakeBodyProvider}, // nil bodyJSON remains nil {nil, nil, nil}, } for _, c := range cases { sling := New() sling.bodyProvider = c.initial sling.BodyJSON(c.input) if sling.bodyProvider != c.expected { t.Errorf("expected %v, got %v", c.expected, sling.bodyProvider) } // 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)) } } } func TestBodyFormSetter(t *testing.T) { fakeParams := FakeParams{KindName: "recent", Count: 25} fakeBodyProvider := formBodyProvider{payload: fakeParams} cases := []struct { initial BodyProvider input interface{} expected BodyProvider }{ // url tagged struct is set as bodyStruct {nil, paramsB, fakeBodyProvider}, // nil argument to bodyStruct does not replace existing bodyStruct {fakeBodyProvider, nil, fakeBodyProvider}, // nil bodyStruct remains nil {nil, nil, nil}, } for _, c := range cases { sling := New() sling.bodyProvider = c.initial sling.BodyForm(c.input) if sling.bodyProvider != c.expected { t.Errorf("expected %v, got %v", c.expected, sling.bodyProvider) } // 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)) } } } func TestBodySetter(t *testing.T) { fakeInput := ioutil.NopCloser(strings.NewReader("test")) fakeBodyProvider := bodyProvider{body: fakeInput} cases := []struct { initial BodyProvider input io.Reader expected BodyProvider }{ // nil body is overriden by a set body {nil, fakeInput, fakeBodyProvider}, // initial body is not overriden by nil body {fakeBodyProvider, nil, fakeBodyProvider}, // nil body is returned unaltered {nil, nil, nil}, } for _, c := range cases { sling := New() sling.bodyProvider = c.initial sling.Body(c.input) if sling.bodyProvider != c.expected { t.Errorf("expected %v, got %v", c.expected, sling.bodyProvider) } } } func TestRequest_urlAndMethod(t *testing.T) { cases := []struct { sling *Sling expectedMethod string expectedURL string expectedErr error }{ {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"), "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"), "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"), "GET", "http://b.io", nil}, // Path setters are additive {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}, } for _, c := range cases { req, err := c.sling.Request() if err != c.expectedErr { t.Errorf("expected error %v, got %v for %+v", c.expectedErr, err, c.sling) } if req.URL.String() != c.expectedURL { t.Errorf("expected url %s, got %s for %+v", c.expectedURL, req.URL.String(), c.sling) } if req.Method != c.expectedMethod { t.Errorf("expected method %s, got %s for %+v", c.expectedMethod, req.Method, c.sling) } } } func TestRequest_queryStructs(t *testing.T) { cases := []struct { sling *Sling expectedURL string }{ {New().Base("http://a.io").QueryStruct(paramsA), "http://a.io?limit=30"}, {New().Base("http://a.io").QueryStruct(paramsA).QueryStruct(paramsB), "http://a.io?count=25&kind_name=recent&limit=30"}, {New().Base("http://a.io/").Path("foo?path=yes").QueryStruct(paramsA), "http://a.io/foo?limit=30&path=yes"}, {New().Base("http://a.io").QueryStruct(paramsA).New(), "http://a.io?limit=30"}, {New().Base("http://a.io").QueryStruct(paramsA).New().QueryStruct(paramsB), "http://a.io?count=25&kind_name=recent&limit=30"}, } for _, c := range cases { req, _ := c.sling.Request() if req.URL.String() != c.expectedURL { t.Errorf("expected url %s, got %s for %+v", c.expectedURL, req.URL.String(), c.sling) } } } func TestRequest_body(t *testing.T) { cases := []struct { sling *Sling expectedBody string // expected Body io.Reader as a string expectedContentType string }{ // 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}, // Body {New().Body(strings.NewReader("this-is-a-test")), "this-is-a-test", ""}, {New().Body(strings.NewReader("a")).Body(strings.NewReader("b")), "b", ""}, } for _, c := range cases { req, _ := c.sling.Request() buf := new(bytes.Buffer) buf.ReadFrom(req.Body) // req.Body should have contained the expectedBody string if value := buf.String(); value != c.expectedBody { t.Errorf("expected Request.Body %s, got %s", c.expectedBody, value) } // Header Content-Type should be expectedContentType ("" means no contentType expected) if actualHeader := req.Header.Get(contentType); actualHeader != c.expectedContentType && c.expectedContentType != "" { t.Errorf("Incorrect or missing header, expected %s, got %s", c.expectedContentType, actualHeader) } } } func TestRequest_bodyNoData(t *testing.T) { // test that Body is left nil when no bodyJSON or bodyStruct set slings := []*Sling{ New(), 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 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) } } } func TestRequest_bodyEncodeErrors(t *testing.T) { cases := []struct { sling *Sling expectedErr error }{ // check that Encode errors are propagated, illegal JSON field {New().BodyJSON(FakeModel{Temperature: math.Inf(1)}), errors.New("json: unsupported value: +Inf")}, } for _, c := range cases { req, err := c.sling.Request() if err == nil || err.Error() != c.expectedErr.Error() { t.Errorf("expected error %v, got %v", c.expectedErr, err) } if req != nil { t.Errorf("expected nil Request, got %+v", req) } } } func TestRequest_headers(t *testing.T) { cases := []struct { sling *Sling expectedHeader map[string][]string }{ {New().Add("authorization", "OAuth key=\"value\""), map[string][]string{"Authorization": []string{"OAuth key=\"value\""}}}, // header keys should be canonicalized {New().Add("content-tYPE", "application/json").Add("User-AGENT", "sling"), map[string][]string{"Content-Type": []string{"application/json"}, "User-Agent": []string{"sling"}}}, // values for existing keys should be appended {New().Add("A", "B").Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}}, // Add should add to values for keys added by parent Slings {New().Add("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}}, {New().Add("A", "B").New().Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}}, // Add and Set {New().Add("A", "B").Set("a", "c"), map[string][]string{"A": []string{"c"}}}, {New().Set("content-type", "A").Set("Content-Type", "B"), map[string][]string{"Content-Type": []string{"B"}}}, // Set should replace values received by copying parent Slings {New().Set("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}}, {New().Add("A", "B").New().Set("a", "c"), map[string][]string{"A": []string{"c"}}}, } for _, c := range cases { req, _ := c.sling.Request() // type conversion from Header to alias'd map for deep equality comparison headerMap := map[string][]string(req.Header) if !reflect.DeepEqual(c.expectedHeader, headerMap) { t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap) } } } func TestAddQueryStructs(t *testing.T) { cases := []struct { rawurl string queryStructs []interface{} expected string }{ {"http://a.io", []interface{}{}, "http://a.io"}, {"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) {"http://a.io?initial=7", []interface{}{paramsA}, "http://a.io?initial=7&limit=30"}, } for _, c := range cases { reqURL, _ := url.Parse(c.rawurl) addQueryStructs(reqURL, c.queryStructs) if reqURL.String() != c.expected { t.Errorf("expected %s, got %s", c.expected, reqURL.String()) } } } // 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", "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) } if resp.StatusCode != 200 { t.Errorf("expected %d, got %d", 200, resp.StatusCode) } if model.Text != expectedText { t.Errorf("expected %s, got %s", expectedText, model.Text) } if model.FavoriteCount != expectedFavoriteCount { t.Errorf("expected %d, got %d", expectedFavoriteCount, model.FavoriteCount) } } 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", "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) } if resp.StatusCode != 200 { t.Errorf("expected %d, got %d", 200, resp.StatusCode) } expected := &APIError{} if !reflect.DeepEqual(expected, apiError) { t.Errorf("failureV should not be populated, exepcted %v, got %v", expected, apiError) } } 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.Header().Set("Content-Type", "application/json") w.WriteHeader(400) 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) } } func TestDo_onFailureWithNilValue(t *testing.T) { client, mux, server := testServer() defer server.Close() mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(420) 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) } } 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) w.Header().Set("Content-Type", "application/json") 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).BodyForm(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.Header().Set("Content-Type", "application/json") 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).BodyForm(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().BodyJSON(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 // 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, 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) } } sling-1.1.0/test000077500000000000000000000007401302546673100135420ustar00rootroot00000000000000#!/usr/bin/env bash set -e PKGS=$(go list ./... | grep -v /examples) FORMATTABLE="$(find . -maxdepth 1 -type d)" LINTABLE=$(go list ./...) go test $PKGS -cover go vet $PKGS echo "Checking gofmt..." fmtRes=$(gofmt -l $FORMATTABLE) if [ -n "${fmtRes}" ]; then echo -e "gofmt checking failed:\n${fmtRes}" exit 2 fi echo "Checking golint..." lintRes=$(echo $LINTABLE | xargs -n 1 golint) if [ -n "${lintRes}" ]; then echo -e "golint checking failed:\n${lintRes}" exit 2 fi