├── .github
├── dependabot.yaml
├── release.yaml
└── workflows
│ └── test.yaml
├── CHANGES.md
├── LICENSE
├── Makefile
├── README.md
├── body.go
├── doc.go
├── examples
├── README.md
├── github.go
├── go.mod
└── go.sum
├── go.mod
├── go.sum
├── response.go
├── sling.go
└── sling_test.go
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | - package-ecosystem: gomod
8 | directory: "/examples"
9 | schedule:
10 | interval: daily
11 |
--------------------------------------------------------------------------------
/.github/release.yaml:
--------------------------------------------------------------------------------
1 | changelog:
2 | categories:
3 | - title: Contributions
4 | labels:
5 | - '*'
6 | exclude:
7 | labels:
8 | - dependencies
9 | - no-release-note
10 | - title: Dependencies
11 | labels:
12 | - dependencies
13 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | branches:
8 | - main
9 | jobs:
10 | go:
11 | uses: dghubble/.github/.github/workflows/golang-library.yaml@main
12 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | # Sling Changelog
2 |
3 | Notable changes between releases.
4 |
5 | ## Latest
6 |
7 | ## v1.4.2
8 |
9 | * Update Go module dependencies
10 |
11 | ## v1.4.1
12 |
13 | * Update minimum Go version to v1.18 ([#76](https://github.com/dghubble/sling/pull/76))
14 |
15 | ## v1.4.0
16 |
17 | * `Do` reads Body to reuse HTTP/1.x "keep-alive" TCP connections ([#59](https://github.com/dghubble/sling/pull/59))
18 | * `Receive` skips decoding if status is 204 (no content) ([#63](https://github.com/dghubble/sling/pull/63))
19 |
20 | ## v1.3.0
21 |
22 | * Add Sling `ResponseDecoder` setter for receiving responses with a custom `ResponseDecoder` ([#49](https://github.com/dghubble/sling/pull/49))
23 | * Add Go module support (i.e. `go.mod`). Exclude `examples` (multi-module). ([#52](https://github.com/dghubble/sling/pull/52))
24 |
25 | ## v1.2.0
26 |
27 | * Add `Connect`, `Options`, and `Trace` HTTP methods ([c51967](https://github.com/dghubble/sling/commit/c519674860ff275e0ceb12caf5d87b31765c4e71))
28 | * Skip receiving (i.e. decoding) `204 No Content` responses ([#31](https://github.com/dghubble/sling/pull/31))
29 |
30 | ## v1.1.0
31 |
32 | * Allow JSON decoding, regardless of response Content-Type (#26)
33 | * Add `BodyProvider` interface and setter so request Body encoding can be customized (#23)
34 | * Add `Doer` interface and setter so request sending behavior can be customized (#21)
35 | * Add `SetBasicAuth` setter for Authorization headers (#16)
36 | * Add Sling `Body` setter to set an `io.Reader` on the Request (#9)
37 |
38 | ## v1.0.0
39 |
40 | * Added support for receiving and decoding error JSON structs
41 | * Renamed Sling `JsonBody` setter to `BodyJSON` (breaking)
42 | * Renamed Sling `BodyStruct` setter to `BodyForm` (breaking)
43 | * Renamed Sling fields `httpClient`, `method`, `rawURL`, and `header` to be internal (breaking)
44 | * Changed `Do` and `Receive` to skip response JSON decoding if "application/json" Content-Type is missing
45 | * Changed `Sling.Receive(v interface{})` to `Sling.Receive(successV, failureV interface{})` (breaking)
46 | * Previously `Receive` attempted to decode the response Body in all cases
47 | * 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.
48 | * 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)`)
49 | * To retain the old behavior, duplicate the first argument (e.g. s.Receive(&tweet, &tweet))
50 | * Changed `Sling.Do(http.Request, v interface{})` to `Sling.Do(http.Request, successV, failureV interface{})` (breaking)
51 | * See the changelog entry about `Receive`, the upgrade path is the same.
52 | * Removed HEAD, GET, POST, PUT, PATCH, DELETE constants, no reason to export them (breaking)
53 |
54 | ## v0.4.0
55 |
56 | * Improved golint compliance
57 | * Fixed typos and test printouts
58 |
59 | ## v0.3.0
60 |
61 | * Added BodyStruct method for setting a url encoded form body on the Request
62 | * Added Add and Set methods for adding or setting Request Headers
63 | * Added JsonBody method for setting JSON Request Body
64 | * Improved examples and documentation
65 |
66 | ## v0.2.0
67 |
68 | * Added http.Client setter
69 | * Added Sling.New() method to return a copy of a Sling
70 | * Added Base setter and Path extension support
71 | * Added method setters (Get, Post, Put, Patch, Delete, Head)
72 | * Added support for encoding URL Query parameters
73 | * Added example tiny Github API
74 | * Changed v0.1.0 method signatures and names (breaking)
75 | * Removed Go 1.0 support
76 |
77 | ## v0.1.0
78 |
79 | * Support decoding JSON responses.
80 |
81 |
82 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Dalton Hubble
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: all
2 | all: test vet fmt
3 |
4 | .PHONY: test
5 | test:
6 | @go test . -cover
7 |
8 | .PHONY: vet
9 | vet:
10 | @go vet -all .
11 |
12 | .PHONY: fmt
13 | fmt:
14 | @test -z $$(go fmt ./...)
15 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sling
2 | [](https://pkg.go.dev/github.com/dghubble/sling)
3 | [](https://github.com/dghubble/sling/actions/workflows/test.yaml?query=branch%3Amain)
4 | [](https://github.com/sponsors/dghubble)
5 | [](https://fosstodon.org/@typhoon)
6 |
7 |
8 |
9 | Sling is a Go HTTP client library for creating and sending API requests.
10 |
11 | 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.
12 |
13 | ### Features
14 |
15 | * Method Setters: Get/Post/Put/Patch/Delete/Head
16 | * Add or Set Request Headers
17 | * Base/Path: Extend a Sling for different endpoints
18 | * Encode structs into URL query parameters
19 | * Encode a form or JSON into the Request Body
20 | * Receive JSON success or failure responses
21 |
22 | ## Install
23 |
24 | ```
25 | go get github.com/dghubble/sling
26 | ```
27 |
28 | ## Documentation
29 |
30 | Read [GoDoc](https://godoc.org/github.com/dghubble/sling)
31 |
32 | ## Usage
33 |
34 | Use a Sling to set path, method, header, query, or body properties and create an `http.Request`.
35 |
36 | ```go
37 | type Params struct {
38 | Count int `url:"count,omitempty"`
39 | }
40 | params := &Params{Count: 5}
41 |
42 | req, err := sling.New().Get("https://example.com").QueryStruct(params).Request()
43 | client.Do(req)
44 | ```
45 |
46 | ### Path
47 |
48 | Use `Path` to set or extend the URL for created Requests. Extension means the path will be resolved relative to the existing URL.
49 |
50 | ```go
51 | // creates a GET request to https://example.com/foo/bar
52 | req, err := sling.New().Base("https://example.com/").Path("foo/").Path("bar").Request()
53 | ```
54 |
55 | Use `Get`, `Post`, `Put`, `Patch`, `Delete`, `Head`, `Options`, `Trace`, or `Connect` which are exactly the same as `Path` except they set the HTTP method too.
56 |
57 | ```go
58 | req, err := sling.New().Post("http://upload.com/gophers")
59 | ```
60 |
61 | ### Headers
62 |
63 | `Add` or `Set` headers for requests created by a Sling.
64 |
65 | ```go
66 | s := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client")
67 | req, err := s.New().Get("gophergram/list").Request()
68 | ```
69 |
70 | ### Query
71 |
72 | #### QueryStruct
73 |
74 | Define [url tagged structs](https://godoc.org/github.com/google/go-querystring/query). Use `QueryStruct` to encode a struct as query parameters on requests.
75 |
76 | ```go
77 | // Github Issue Parameters
78 | type IssueParams struct {
79 | Filter string `url:"filter,omitempty"`
80 | State string `url:"state,omitempty"`
81 | Labels string `url:"labels,omitempty"`
82 | Sort string `url:"sort,omitempty"`
83 | Direction string `url:"direction,omitempty"`
84 | Since string `url:"since,omitempty"`
85 | }
86 | ```
87 |
88 | ```go
89 | githubBase := sling.New().Base("https://api.github.com/").Client(httpClient)
90 |
91 | path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
92 | params := &IssueParams{Sort: "updated", State: "open"}
93 | req, err := githubBase.New().Get(path).QueryStruct(params).Request()
94 | ```
95 |
96 | ### Body
97 |
98 | #### JSON Body
99 |
100 | Define [JSON tagged structs](https://golang.org/pkg/encoding/json/). Use `BodyJSON` to JSON encode a struct as the Body on requests.
101 |
102 | ```go
103 | type IssueRequest struct {
104 | Title string `json:"title,omitempty"`
105 | Body string `json:"body,omitempty"`
106 | Assignee string `json:"assignee,omitempty"`
107 | Milestone int `json:"milestone,omitempty"`
108 | Labels []string `json:"labels,omitempty"`
109 | }
110 | ```
111 |
112 | ```go
113 | githubBase := sling.New().Base("https://api.github.com/").Client(httpClient)
114 | path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
115 |
116 | body := &IssueRequest{
117 | Title: "Test title",
118 | Body: "Some issue",
119 | }
120 | req, err := githubBase.New().Post(path).BodyJSON(body).Request()
121 | ```
122 |
123 | Requests will include an `application/json` Content-Type header.
124 |
125 | #### Form Body
126 |
127 | 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.
128 |
129 | ```go
130 | type StatusUpdateParams struct {
131 | Status string `url:"status,omitempty"`
132 | InReplyToStatusId int64 `url:"in_reply_to_status_id,omitempty"`
133 | MediaIds []int64 `url:"media_ids,omitempty,comma"`
134 | }
135 | ```
136 |
137 | ```go
138 | tweetParams := &StatusUpdateParams{Status: "writing some Go"}
139 | req, err := twitterBase.New().Post(path).BodyForm(tweetParams).Request()
140 | ```
141 |
142 | Requests will include an `application/x-www-form-urlencoded` Content-Type header.
143 |
144 | #### Plain Body
145 |
146 | Use `Body` to set a plain `io.Reader` on requests created by a Sling.
147 |
148 | ```go
149 | body := strings.NewReader("raw body")
150 | req, err := sling.New().Base("https://example.com").Body(body).Request()
151 | ```
152 |
153 | Set a content type header, if desired (e.g. `Set("Content-Type", "text/plain")`).
154 |
155 | ### Extend a Sling
156 |
157 | Each Sling creates a standard `http.Request` (e.g. with some path and query
158 | 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).
159 |
160 | Each Sling instance provides a `New()` method which creates an independent copy, so setting properties on the child won't mutate the parent Sling.
161 |
162 | ```go
163 | const twitterApi = "https://api.twitter.com/1.1/"
164 | base := sling.New().Base(twitterApi).Client(authClient)
165 |
166 | // statuses/show.json Sling
167 | tweetShowSling := base.New().Get("statuses/show.json").QueryStruct(params)
168 | req, err := tweetShowSling.Request()
169 |
170 | // statuses/update.json Sling
171 | tweetPostSling := base.New().Post("statuses/update.json").BodyForm(params)
172 | req, err := tweetPostSling.Request()
173 | ```
174 |
175 | Without the calls to `base.New()`, `tweetShowSling` and `tweetPostSling` would reference the base Sling and POST to
176 | "https://api.twitter.com/1.1/statuses/show.json/statuses/update.json", which
177 | is undesired.
178 |
179 | Recap: If you wish to *extend* a Sling, create a new child copy with `New()`.
180 |
181 | ### Sending
182 |
183 | #### Receive
184 |
185 | 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.
186 |
187 | ```go
188 | // Github Issue (abbreviated)
189 | type Issue struct {
190 | Title string `json:"title"`
191 | Body string `json:"body"`
192 | }
193 | ```
194 |
195 | ```go
196 | issues := new([]Issue)
197 | resp, err := githubBase.New().Get(path).QueryStruct(params).ReceiveSuccess(issues)
198 | fmt.Println(issues, resp, err)
199 | ```
200 |
201 | 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.
202 |
203 | ```go
204 | type GithubError struct {
205 | Message string `json:"message"`
206 | Errors []struct {
207 | Resource string `json:"resource"`
208 | Field string `json:"field"`
209 | Code string `json:"code"`
210 | } `json:"errors"`
211 | DocumentationURL string `json:"documentation_url"`
212 | }
213 | ```
214 |
215 | ```go
216 | issues := new([]Issue)
217 | githubError := new(GithubError)
218 | resp, err := githubBase.New().Get(path).QueryStruct(params).Receive(issues, githubError)
219 | fmt.Println(issues, githubError, resp, err)
220 | ```
221 |
222 | Pass a nil `successV` or `failureV` argument to skip JSON decoding into that value.
223 |
224 | ### Modify a Request
225 |
226 | Sling provides the raw http.Request so modifications can be made using standard net/http features. For example, in Go 1.7+ , add HTTP tracing to a request with a context:
227 |
228 | ```go
229 | req, err := sling.New().Get("https://example.com").QueryStruct(params).Request()
230 | // handle error
231 |
232 | trace := &httptrace.ClientTrace{
233 | DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
234 | fmt.Printf("DNS Info: %+v\n", dnsInfo)
235 | },
236 | GotConn: func(connInfo httptrace.GotConnInfo) {
237 | fmt.Printf("Got Conn: %+v\n", connInfo)
238 | },
239 | }
240 |
241 | req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
242 | client.Do(req)
243 | ```
244 |
245 | ### Build an API
246 |
247 | 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.
248 |
249 | ```go
250 | const baseURL = "https://api.github.com/"
251 |
252 | type IssueService struct {
253 | sling *sling.Sling
254 | }
255 |
256 | func NewIssueService(httpClient *http.Client) *IssueService {
257 | return &IssueService{
258 | sling: sling.New().Client(httpClient).Base(baseURL),
259 | }
260 | }
261 |
262 | func (s *IssueService) ListByRepo(owner, repo string, params *IssueListParams) ([]Issue, *http.Response, error) {
263 | issues := new([]Issue)
264 | githubError := new(GithubError)
265 | path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
266 | resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(issues, githubError)
267 | if err == nil {
268 | err = githubError
269 | }
270 | return *issues, resp, err
271 | }
272 | ```
273 |
274 | ## Example APIs using Sling
275 |
276 | * Digits [dghubble/go-digits](https://github.com/dghubble/go-digits)
277 | * GoSquared [drinkin/go-gosquared](https://github.com/drinkin/go-gosquared)
278 | * Kala [ajvb/kala](https://github.com/ajvb/kala)
279 | * Parse [fergstar/go-parse](https://github.com/fergstar/go-parse)
280 | * Swagger Generator [swagger-api/swagger-codegen](https://github.com/swagger-api/swagger-codegen)
281 | * Twitter [dghubble/go-twitter](https://github.com/dghubble/go-twitter)
282 | * Stacksmith [jesustinoco/go-smith](https://github.com/jesustinoco/go-smith)
283 | * Spotify [omegastreamtv/Spotify](https://github.com/omegastreamtv/Spotify)
284 |
285 | Create a Pull Request to add a link to your own API.
286 |
287 | ## Motivation
288 |
289 | 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.
290 |
291 | This project borrows and abstracts those ideas into a Sling, an agnostic component any API client can use for creating and sending requests.
292 |
293 | ## Contributing
294 |
295 | See the [Contributing Guide](https://gist.github.com/dghubble/be682c123727f70bcfe7).
296 |
297 | ## License
298 |
299 | [MIT License](LICENSE)
300 |
--------------------------------------------------------------------------------
/body.go:
--------------------------------------------------------------------------------
1 | package sling
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "strings"
8 |
9 | goquery "github.com/google/go-querystring/query"
10 | )
11 |
12 | // BodyProvider provides Body content for http.Request attachment.
13 | type BodyProvider interface {
14 | // ContentType returns the Content-Type of the body.
15 | ContentType() string
16 | // Body returns the io.Reader body.
17 | Body() (io.Reader, error)
18 | }
19 |
20 | // bodyProvider provides the wrapped body value as a Body for reqests.
21 | type bodyProvider struct {
22 | body io.Reader
23 | }
24 |
25 | func (p bodyProvider) ContentType() string {
26 | return ""
27 | }
28 |
29 | func (p bodyProvider) Body() (io.Reader, error) {
30 | return p.body, nil
31 | }
32 |
33 | // jsonBodyProvider encodes a JSON tagged struct value as a Body for requests.
34 | // See https://golang.org/pkg/encoding/json/#MarshalIndent for details.
35 | type jsonBodyProvider struct {
36 | payload interface{}
37 | }
38 |
39 | func (p jsonBodyProvider) ContentType() string {
40 | return jsonContentType
41 | }
42 |
43 | func (p jsonBodyProvider) Body() (io.Reader, error) {
44 | buf := &bytes.Buffer{}
45 | err := json.NewEncoder(buf).Encode(p.payload)
46 | if err != nil {
47 | return nil, err
48 | }
49 | return buf, nil
50 | }
51 |
52 | // formBodyProvider encodes a url tagged struct value as Body for requests.
53 | // See https://godoc.org/github.com/google/go-querystring/query for details.
54 | type formBodyProvider struct {
55 | payload interface{}
56 | }
57 |
58 | func (p formBodyProvider) ContentType() string {
59 | return formContentType
60 | }
61 |
62 | func (p formBodyProvider) Body() (io.Reader, error) {
63 | values, err := goquery.Values(p.payload)
64 | if err != nil {
65 | return nil, err
66 | }
67 | return strings.NewReader(values.Encode()), nil
68 | }
69 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package sling is a Go HTTP client library for creating and sending API requests.
3 |
4 | Slings store HTTP Request properties to simplify sending requests and decoding
5 | responses. Check the examples to learn how to compose a Sling into your API
6 | client.
7 |
8 | # Usage
9 |
10 | Use a Sling to set path, method, header, query, or body properties and create an
11 | http.Request.
12 |
13 | type Params struct {
14 | Count int `url:"count,omitempty"`
15 | }
16 | params := &Params{Count: 5}
17 |
18 | req, err := sling.New().Get("https://example.com").QueryStruct(params).Request()
19 | client.Do(req)
20 |
21 | # Path
22 |
23 | Use Path to set or extend the URL for created Requests. Extension means the
24 | path will be resolved relative to the existing URL.
25 |
26 | // creates a GET request to https://example.com/foo/bar
27 | req, err := sling.New().Base("https://example.com/").Path("foo/").Path("bar").Request()
28 |
29 | Use Get, Post, Put, Patch, Delete, or Head which are exactly the same as Path
30 | except they set the HTTP method too.
31 |
32 | req, err := sling.New().Post("http://upload.com/gophers")
33 |
34 | # Headers
35 |
36 | Add or Set headers for requests created by a Sling.
37 |
38 | s := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client")
39 | req, err := s.New().Get("gophergram/list").Request()
40 |
41 | # QueryStruct
42 |
43 | Define url parameter structs (https://godoc.org/github.com/google/go-querystring/query).
44 | Use QueryStruct to encode a struct as query parameters on requests.
45 |
46 | // Github Issue Parameters
47 | type IssueParams struct {
48 | Filter string `url:"filter,omitempty"`
49 | State string `url:"state,omitempty"`
50 | Labels string `url:"labels,omitempty"`
51 | Sort string `url:"sort,omitempty"`
52 | Direction string `url:"direction,omitempty"`
53 | Since string `url:"since,omitempty"`
54 | }
55 |
56 | githubBase := sling.New().Base("https://api.github.com/").Client(httpClient)
57 |
58 | path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
59 | params := &IssueParams{Sort: "updated", State: "open"}
60 | req, err := githubBase.New().Get(path).QueryStruct(params).Request()
61 |
62 | # Json Body
63 |
64 | Define JSON tagged structs (https://golang.org/pkg/encoding/json/).
65 | Use BodyJSON to JSON encode a struct as the Body on requests.
66 |
67 | type IssueRequest struct {
68 | Title string `json:"title,omitempty"`
69 | Body string `json:"body,omitempty"`
70 | Assignee string `json:"assignee,omitempty"`
71 | Milestone int `json:"milestone,omitempty"`
72 | Labels []string `json:"labels,omitempty"`
73 | }
74 |
75 | githubBase := sling.New().Base("https://api.github.com/").Client(httpClient)
76 | path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
77 |
78 | body := &IssueRequest{
79 | Title: "Test title",
80 | Body: "Some issue",
81 | }
82 | req, err := githubBase.New().Post(path).BodyJSON(body).Request()
83 |
84 | Requests will include an "application/json" Content-Type header.
85 |
86 | # Form Body
87 |
88 | Define url tagged structs (https://godoc.org/github.com/google/go-querystring/query).
89 | Use BodyForm to form url encode a struct as the Body on requests.
90 |
91 | type StatusUpdateParams struct {
92 | Status string `url:"status,omitempty"`
93 | InReplyToStatusId int64 `url:"in_reply_to_status_id,omitempty"`
94 | MediaIds []int64 `url:"media_ids,omitempty,comma"`
95 | }
96 |
97 | tweetParams := &StatusUpdateParams{Status: "writing some Go"}
98 | req, err := twitterBase.New().Post(path).BodyForm(tweetParams).Request()
99 |
100 | Requests will include an "application/x-www-form-urlencoded" Content-Type
101 | header.
102 |
103 | # Plain Body
104 |
105 | Use Body to set a plain io.Reader on requests created by a Sling.
106 |
107 | body := strings.NewReader("raw body")
108 | req, err := sling.New().Base("https://example.com").Body(body).Request()
109 |
110 | Set a content type header, if desired (e.g. Set("Content-Type", "text/plain")).
111 |
112 | # Extend a Sling
113 |
114 | Each Sling generates an http.Request (say with some path and query params)
115 | each time Request() is called, based on its state. When creating
116 | different slings, you may wish to extend an existing Sling to minimize
117 | duplication (e.g. a common client).
118 |
119 | Each Sling instance provides a New() method which creates an independent copy,
120 | so setting properties on the child won't mutate the parent Sling.
121 |
122 | const twitterApi = "https://api.twitter.com/1.1/"
123 | base := sling.New().Base(twitterApi).Client(authClient)
124 |
125 | // statuses/show.json Sling
126 | tweetShowSling := base.New().Get("statuses/show.json").QueryStruct(params)
127 | req, err := tweetShowSling.Request()
128 |
129 | // statuses/update.json Sling
130 | tweetPostSling := base.New().Post("statuses/update.json").BodyForm(params)
131 | req, err := tweetPostSling.Request()
132 |
133 | Without the calls to base.New(), tweetShowSling and tweetPostSling would
134 | reference the base Sling and POST to
135 | "https://api.twitter.com/1.1/statuses/show.json/statuses/update.json", which
136 | is undesired.
137 |
138 | Recap: If you wish to extend a Sling, create a new child copy with New().
139 |
140 | # Receive
141 |
142 | Define a JSON struct to decode a type from 2XX success responses. Use
143 | ReceiveSuccess(successV interface{}) to send a new Request and decode the
144 | response body into successV if it succeeds.
145 |
146 | // Github Issue (abbreviated)
147 | type Issue struct {
148 | Title string `json:"title"`
149 | Body string `json:"body"`
150 | }
151 |
152 | issues := new([]Issue)
153 | resp, err := githubBase.New().Get(path).QueryStruct(params).ReceiveSuccess(issues)
154 | fmt.Println(issues, resp, err)
155 |
156 | Most APIs return failure responses with JSON error details. To decode these,
157 | define success and failure JSON structs. Use
158 | Receive(successV, failureV interface{}) to send a new Request that will
159 | automatically decode the response into the successV for 2XX responses or into
160 | failureV for non-2XX responses.
161 |
162 | type GithubError struct {
163 | Message string `json:"message"`
164 | Errors []struct {
165 | Resource string `json:"resource"`
166 | Field string `json:"field"`
167 | Code string `json:"code"`
168 | } `json:"errors"`
169 | DocumentationURL string `json:"documentation_url"`
170 | }
171 |
172 | issues := new([]Issue)
173 | githubError := new(GithubError)
174 | resp, err := githubBase.New().Get(path).QueryStruct(params).Receive(issues, githubError)
175 | fmt.Println(issues, githubError, resp, err)
176 |
177 | Pass a nil successV or failureV argument to skip JSON decoding into that value.
178 | */
179 | package sling
180 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## Example API Client with Sling
3 |
4 | Try the example Github API Client.
5 |
6 | cd examples
7 | go get .
8 |
9 | List the public issues on the [github.com/golang/go](https://github.com/golang/go) repository.
10 |
11 | go run github.go
12 |
13 | To list your public and private Github issues, pass your [Github Access Token](https://github.com/settings/tokens)
14 |
15 | go run github.go -access-token=xxx
16 |
17 | or set the `GITHUB_ACCESS_TOKEN` environment variable.
18 |
19 | For a complete Github API, see the excellent [google/go-github](https://github.com/google/go-github) package.
--------------------------------------------------------------------------------
/examples/github.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/coreos/pkg/flagutil"
11 | "github.com/dghubble/sling"
12 | "golang.org/x/oauth2"
13 | )
14 |
15 | const baseURL = "https://api.github.com/"
16 |
17 | // Issue is a simplified Github issue
18 | // https://developer.github.com/v3/issues/#response
19 | type Issue struct {
20 | ID int `json:"id"`
21 | URL string `json:"url"`
22 | Number int `json:"number"`
23 | State string `json:"state"`
24 | Title string `json:"title"`
25 | Body string `json:"body"`
26 | }
27 |
28 | // GithubError represents a Github API error response
29 | // https://developer.github.com/v3/#client-errors
30 | type GithubError struct {
31 | Message string `json:"message"`
32 | Errors []struct {
33 | Resource string `json:"resource"`
34 | Field string `json:"field"`
35 | Code string `json:"code"`
36 | } `json:"errors"`
37 | DocumentationURL string `json:"documentation_url"`
38 | }
39 |
40 | func (e GithubError) Error() string {
41 | return fmt.Sprintf("github: %v %+v %v", e.Message, e.Errors, e.DocumentationURL)
42 | }
43 |
44 | // IssueRequest is a simplified issue request
45 | // https://developer.github.com/v3/issues/#create-an-issue
46 | type IssueRequest struct {
47 | Title string `json:"title,omitempty"`
48 | Body string `json:"body,omitempty"`
49 | Assignee string `json:"assignee,omitempty"`
50 | Milestone int `json:"milestone,omitempty"`
51 | Labels []string `json:"labels,omitempty"`
52 | }
53 |
54 | // IssueListParams are the params for IssueService.List
55 | // https://developer.github.com/v3/issues/#parameters
56 | type IssueListParams struct {
57 | Filter string `url:"filter,omitempty"`
58 | State string `url:"state,omitempty"`
59 | Labels string `url:"labels,omitempty"`
60 | Sort string `url:"sort,omitempty"`
61 | Direction string `url:"direction,omitempty"`
62 | Since string `url:"since,omitempty"`
63 | }
64 |
65 | // Services
66 |
67 | // IssueService provides methods for creating and reading issues.
68 | type IssueService struct {
69 | sling *sling.Sling
70 | }
71 |
72 | // NewIssueService returns a new IssueService.
73 | func NewIssueService(httpClient *http.Client) *IssueService {
74 | return &IssueService{
75 | sling: sling.New().Client(httpClient).Base(baseURL),
76 | }
77 | }
78 |
79 | // List returns the authenticated user's issues across repos and orgs.
80 | func (s *IssueService) List(params *IssueListParams) ([]Issue, *http.Response, error) {
81 | issues := new([]Issue)
82 | githubError := new(GithubError)
83 | resp, err := s.sling.New().Path("issues").QueryStruct(params).Receive(issues, githubError)
84 | if err == nil {
85 | err = githubError
86 | }
87 | return *issues, resp, err
88 | }
89 |
90 | // ListByRepo returns a repository's issues.
91 | func (s *IssueService) ListByRepo(owner, repo string, params *IssueListParams) ([]Issue, *http.Response, error) {
92 | issues := new([]Issue)
93 | githubError := new(GithubError)
94 | path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
95 | resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(issues, githubError)
96 | if err == nil {
97 | err = githubError
98 | }
99 | return *issues, resp, err
100 | }
101 |
102 | // Create creates a new issue on the specified repository.
103 | func (s *IssueService) Create(owner, repo string, issueBody *IssueRequest) (*Issue, *http.Response, error) {
104 | issue := new(Issue)
105 | githubError := new(GithubError)
106 | path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
107 | resp, err := s.sling.New().Post(path).BodyJSON(issueBody).Receive(issue, githubError)
108 | if err == nil {
109 | err = githubError
110 | }
111 | return issue, resp, err
112 | }
113 |
114 | // Client to wrap services
115 |
116 | // Client is a tiny Github client
117 | type Client struct {
118 | IssueService *IssueService
119 | // other service endpoints...
120 | }
121 |
122 | // NewClient returns a new Client
123 | func NewClient(httpClient *http.Client) *Client {
124 | return &Client{
125 | IssueService: NewIssueService(httpClient),
126 | }
127 | }
128 |
129 | func main() {
130 | // Github Unauthenticated API
131 | client := NewClient(nil)
132 | params := &IssueListParams{Sort: "updated"}
133 | issues, _, _ := client.IssueService.ListByRepo("golang", "go", params)
134 | fmt.Printf("Public golang/go Issues:\n%v\n", issues)
135 |
136 | // Github OAuth2 API
137 | flags := flag.NewFlagSet("github-example", flag.ExitOnError)
138 | // -access-token=xxx or GITHUB_ACCESS_TOKEN env var
139 | accessToken := flags.String("access-token", "", "Github Access Token")
140 | flags.Parse(os.Args[1:])
141 | flagutil.SetFlagsFromEnv(flags, "GITHUB")
142 |
143 | if *accessToken == "" {
144 | log.Fatal("Github Access Token required to list private issues")
145 | }
146 |
147 | config := &oauth2.Config{}
148 | token := &oauth2.Token{AccessToken: *accessToken}
149 | httpClient := config.Client(oauth2.NoContext, token)
150 |
151 | client = NewClient(httpClient)
152 | issues, _, _ = client.IssueService.List(params)
153 | fmt.Printf("Your Github Issues:\n%v\n", issues)
154 |
155 | // body := &IssueRequest{
156 | // Title: "Test title",
157 | // Body: "Some test issue",
158 | // }
159 | // issue, _, _ := client.IssueService.Create("dghubble", "temp", body)
160 | // fmt.Println(issue)
161 | }
162 |
--------------------------------------------------------------------------------
/examples/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dghubble/sling/examples
2 |
3 | go 1.23.0
4 |
5 | toolchain go1.24.0
6 |
7 | require (
8 | github.com/coreos/pkg v0.0.0-20230209195159-6f3db454fdf8
9 | github.com/dghubble/sling v1.4.2
10 | golang.org/x/oauth2 v0.30.0
11 | )
12 |
13 | require github.com/google/go-querystring v1.1.0 // indirect
14 |
--------------------------------------------------------------------------------
/examples/go.sum:
--------------------------------------------------------------------------------
1 | github.com/coreos/pkg v0.0.0-20230209195159-6f3db454fdf8 h1:bu+DLSrk0L1eW0/tNbOMJ9VcHO3jkvtxcncB9wcBa0A=
2 | github.com/coreos/pkg v0.0.0-20230209195159-6f3db454fdf8/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
3 | github.com/dghubble/sling v1.4.2 h1:vs1HIGBbSl2SEALyU+irpYFLZMfc49Fp+jYryFebQjM=
4 | github.com/dghubble/sling v1.4.2/go.mod h1:o0arCOz0HwfqYQJLrRtqunaWOn4X6jxE/6ORKRpVTD4=
5 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
6 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
7 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
8 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
9 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
10 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
11 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
12 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dghubble/sling
2 |
3 | go 1.19
4 |
5 | require github.com/google/go-querystring v1.1.0
6 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
2 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
3 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
4 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
5 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
6 |
--------------------------------------------------------------------------------
/response.go:
--------------------------------------------------------------------------------
1 | package sling
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | )
7 |
8 | // ResponseDecoder decodes http responses into struct values.
9 | type ResponseDecoder interface {
10 | // Decode decodes the response into the value pointed to by v.
11 | Decode(resp *http.Response, v interface{}) error
12 | }
13 |
14 | // jsonDecoder decodes http response JSON into a JSON-tagged struct value.
15 | type jsonDecoder struct {
16 | }
17 |
18 | // Decode decodes the Response Body into the value pointed to by v.
19 | // Caller must provide a non-nil v and close the resp.Body.
20 | func (d jsonDecoder) Decode(resp *http.Response, v interface{}) error {
21 | return json.NewDecoder(resp.Body).Decode(v)
22 | }
23 |
--------------------------------------------------------------------------------
/sling.go:
--------------------------------------------------------------------------------
1 | package sling
2 |
3 | import (
4 | "encoding/base64"
5 | "io"
6 | "net/http"
7 | "net/url"
8 |
9 | goquery "github.com/google/go-querystring/query"
10 | )
11 |
12 | const (
13 | contentType = "Content-Type"
14 | jsonContentType = "application/json"
15 | formContentType = "application/x-www-form-urlencoded"
16 | )
17 |
18 | // Doer executes http requests. It is implemented by *http.Client. You can
19 | // wrap *http.Client with layers of Doers to form a stack of client-side
20 | // middleware.
21 | type Doer interface {
22 | Do(req *http.Request) (*http.Response, error)
23 | }
24 |
25 | // Sling is an HTTP Request builder and sender.
26 | type Sling struct {
27 | // http Client for doing requests
28 | httpClient Doer
29 | // HTTP method (GET, POST, etc.)
30 | method string
31 | // raw url string for requests
32 | rawURL string
33 | // stores key-values pairs to add to request's Headers
34 | header http.Header
35 | // url tagged query structs
36 | queryStructs []interface{}
37 | // body provider
38 | bodyProvider BodyProvider
39 | // response decoder
40 | responseDecoder ResponseDecoder
41 | }
42 |
43 | // New returns a new Sling with an http DefaultClient.
44 | func New() *Sling {
45 | return &Sling{
46 | httpClient: http.DefaultClient,
47 | method: "GET",
48 | header: make(http.Header),
49 | queryStructs: make([]interface{}, 0),
50 | responseDecoder: jsonDecoder{},
51 | }
52 | }
53 |
54 | // New returns a copy of a Sling for creating a new Sling with properties
55 | // from a parent Sling. For example,
56 | //
57 | // parentSling := sling.New().Client(client).Base("https://api.io/")
58 | // fooSling := parentSling.New().Get("foo/")
59 | // barSling := parentSling.New().Get("bar/")
60 | //
61 | // fooSling and barSling will both use the same client, but send requests to
62 | // https://api.io/foo/ and https://api.io/bar/ respectively.
63 | //
64 | // Note that query and body values are copied so if pointer values are used,
65 | // mutating the original value will mutate the value within the child Sling.
66 | func (s *Sling) New() *Sling {
67 | // copy Headers pairs into new Header map
68 | headerCopy := make(http.Header)
69 | for k, v := range s.header {
70 | headerCopy[k] = v
71 | }
72 | return &Sling{
73 | httpClient: s.httpClient,
74 | method: s.method,
75 | rawURL: s.rawURL,
76 | header: headerCopy,
77 | queryStructs: append([]interface{}{}, s.queryStructs...),
78 | bodyProvider: s.bodyProvider,
79 | responseDecoder: s.responseDecoder,
80 | }
81 | }
82 |
83 | // Http Client
84 |
85 | // Client sets the http Client used to do requests. If a nil client is given,
86 | // the http.DefaultClient will be used.
87 | func (s *Sling) Client(httpClient *http.Client) *Sling {
88 | if httpClient == nil {
89 | return s.Doer(http.DefaultClient)
90 | }
91 | return s.Doer(httpClient)
92 | }
93 |
94 | // Doer sets the custom Doer implementation used to do requests.
95 | // If a nil client is given, the http.DefaultClient will be used.
96 | func (s *Sling) Doer(doer Doer) *Sling {
97 | if doer == nil {
98 | s.httpClient = http.DefaultClient
99 | } else {
100 | s.httpClient = doer
101 | }
102 | return s
103 | }
104 |
105 | // Method
106 |
107 | // Head sets the Sling method to HEAD and sets the given pathURL.
108 | func (s *Sling) Head(pathURL string) *Sling {
109 | s.method = "HEAD"
110 | return s.Path(pathURL)
111 | }
112 |
113 | // Get sets the Sling method to GET and sets the given pathURL.
114 | func (s *Sling) Get(pathURL string) *Sling {
115 | s.method = "GET"
116 | return s.Path(pathURL)
117 | }
118 |
119 | // Post sets the Sling method to POST and sets the given pathURL.
120 | func (s *Sling) Post(pathURL string) *Sling {
121 | s.method = "POST"
122 | return s.Path(pathURL)
123 | }
124 |
125 | // Put sets the Sling method to PUT and sets the given pathURL.
126 | func (s *Sling) Put(pathURL string) *Sling {
127 | s.method = "PUT"
128 | return s.Path(pathURL)
129 | }
130 |
131 | // Patch sets the Sling method to PATCH and sets the given pathURL.
132 | func (s *Sling) Patch(pathURL string) *Sling {
133 | s.method = "PATCH"
134 | return s.Path(pathURL)
135 | }
136 |
137 | // Delete sets the Sling method to DELETE and sets the given pathURL.
138 | func (s *Sling) Delete(pathURL string) *Sling {
139 | s.method = "DELETE"
140 | return s.Path(pathURL)
141 | }
142 |
143 | // Options sets the Sling method to OPTIONS and sets the given pathURL.
144 | func (s *Sling) Options(pathURL string) *Sling {
145 | s.method = "OPTIONS"
146 | return s.Path(pathURL)
147 | }
148 |
149 | // Trace sets the Sling method to TRACE and sets the given pathURL.
150 | func (s *Sling) Trace(pathURL string) *Sling {
151 | s.method = "TRACE"
152 | return s.Path(pathURL)
153 | }
154 |
155 | // Connect sets the Sling method to CONNECT and sets the given pathURL.
156 | func (s *Sling) Connect(pathURL string) *Sling {
157 | s.method = "CONNECT"
158 | return s.Path(pathURL)
159 | }
160 |
161 | // Header
162 |
163 | // Add adds the key, value pair in Headers, appending values for existing keys
164 | // to the key's values. Header keys are canonicalized.
165 | func (s *Sling) Add(key, value string) *Sling {
166 | s.header.Add(key, value)
167 | return s
168 | }
169 |
170 | // Set sets the key, value pair in Headers, replacing existing values
171 | // associated with key. Header keys are canonicalized.
172 | func (s *Sling) Set(key, value string) *Sling {
173 | s.header.Set(key, value)
174 | return s
175 | }
176 |
177 | // SetBasicAuth sets the Authorization header to use HTTP Basic Authentication
178 | // with the provided username and password. With HTTP Basic Authentication
179 | // the provided username and password are not encrypted.
180 | func (s *Sling) SetBasicAuth(username, password string) *Sling {
181 | return s.Set("Authorization", "Basic "+basicAuth(username, password))
182 | }
183 |
184 | // basicAuth returns the base64 encoded username:password for basic auth copied
185 | // from net/http.
186 | func basicAuth(username, password string) string {
187 | auth := username + ":" + password
188 | return base64.StdEncoding.EncodeToString([]byte(auth))
189 | }
190 |
191 | // Url
192 |
193 | // Base sets the rawURL. If you intend to extend the url with Path,
194 | // baseUrl should be specified with a trailing slash.
195 | func (s *Sling) Base(rawURL string) *Sling {
196 | s.rawURL = rawURL
197 | return s
198 | }
199 |
200 | // Path extends the rawURL with the given path by resolving the reference to
201 | // an absolute URL. If parsing errors occur, the rawURL is left unmodified.
202 | func (s *Sling) Path(path string) *Sling {
203 | baseURL, baseErr := url.Parse(s.rawURL)
204 | pathURL, pathErr := url.Parse(path)
205 | if baseErr == nil && pathErr == nil {
206 | s.rawURL = baseURL.ResolveReference(pathURL).String()
207 | return s
208 | }
209 | return s
210 | }
211 |
212 | // QueryStruct appends the queryStruct to the Sling's queryStructs. The value
213 | // pointed to by each queryStruct will be encoded as url query parameters on
214 | // new requests (see Request()).
215 | // The queryStruct argument should be a pointer to a url tagged struct. See
216 | // https://godoc.org/github.com/google/go-querystring/query for details.
217 | func (s *Sling) QueryStruct(queryStruct interface{}) *Sling {
218 | if queryStruct != nil {
219 | s.queryStructs = append(s.queryStructs, queryStruct)
220 | }
221 | return s
222 | }
223 |
224 | // Body
225 |
226 | // Body sets the Sling's body. The body value will be set as the Body on new
227 | // requests (see Request()).
228 | // If the provided body is also an io.Closer, the request Body will be closed
229 | // by http.Client methods.
230 | func (s *Sling) Body(body io.Reader) *Sling {
231 | if body == nil {
232 | return s
233 | }
234 | return s.BodyProvider(bodyProvider{body: body})
235 | }
236 |
237 | // BodyProvider sets the Sling's body provider.
238 | func (s *Sling) BodyProvider(body BodyProvider) *Sling {
239 | if body == nil {
240 | return s
241 | }
242 | s.bodyProvider = body
243 |
244 | ct := body.ContentType()
245 | if ct != "" {
246 | s.Set(contentType, ct)
247 | }
248 |
249 | return s
250 | }
251 |
252 | // BodyJSON sets the Sling's bodyJSON. The value pointed to by the bodyJSON
253 | // will be JSON encoded as the Body on new requests (see Request()).
254 | // The bodyJSON argument should be a pointer to a JSON tagged struct. See
255 | // https://golang.org/pkg/encoding/json/#MarshalIndent for details.
256 | func (s *Sling) BodyJSON(bodyJSON interface{}) *Sling {
257 | if bodyJSON == nil {
258 | return s
259 | }
260 | return s.BodyProvider(jsonBodyProvider{payload: bodyJSON})
261 | }
262 |
263 | // BodyForm sets the Sling's bodyForm. The value pointed to by the bodyForm
264 | // will be url encoded as the Body on new requests (see Request()).
265 | // The bodyForm argument should be a pointer to a url tagged struct. See
266 | // https://godoc.org/github.com/google/go-querystring/query for details.
267 | func (s *Sling) BodyForm(bodyForm interface{}) *Sling {
268 | if bodyForm == nil {
269 | return s
270 | }
271 | return s.BodyProvider(formBodyProvider{payload: bodyForm})
272 | }
273 |
274 | // Requests
275 |
276 | // Request returns a new http.Request created with the Sling properties.
277 | // Returns any errors parsing the rawURL, encoding query structs, encoding
278 | // the body, or creating the http.Request.
279 | func (s *Sling) Request() (*http.Request, error) {
280 | reqURL, err := url.Parse(s.rawURL)
281 | if err != nil {
282 | return nil, err
283 | }
284 |
285 | err = addQueryStructs(reqURL, s.queryStructs)
286 | if err != nil {
287 | return nil, err
288 | }
289 |
290 | var body io.Reader
291 | if s.bodyProvider != nil {
292 | body, err = s.bodyProvider.Body()
293 | if err != nil {
294 | return nil, err
295 | }
296 | }
297 | req, err := http.NewRequest(s.method, reqURL.String(), body)
298 | if err != nil {
299 | return nil, err
300 | }
301 | addHeaders(req, s.header)
302 | return req, err
303 | }
304 |
305 | // addQueryStructs parses url tagged query structs using go-querystring to
306 | // encode them to url.Values and format them onto the url.RawQuery. Any
307 | // query parsing or encoding errors are returned.
308 | func addQueryStructs(reqURL *url.URL, queryStructs []interface{}) error {
309 | urlValues, err := url.ParseQuery(reqURL.RawQuery)
310 | if err != nil {
311 | return err
312 | }
313 | // encodes query structs into a url.Values map and merges maps
314 | for _, queryStruct := range queryStructs {
315 | queryValues, err := goquery.Values(queryStruct)
316 | if err != nil {
317 | return err
318 | }
319 | for key, values := range queryValues {
320 | for _, value := range values {
321 | urlValues.Add(key, value)
322 | }
323 | }
324 | }
325 | // url.Values format to a sorted "url encoded" string, e.g. "key=val&foo=bar"
326 | reqURL.RawQuery = urlValues.Encode()
327 | return nil
328 | }
329 |
330 | // addHeaders adds the key, value pairs from the given http.Header to the
331 | // request. Values for existing keys are appended to the keys values.
332 | func addHeaders(req *http.Request, header http.Header) {
333 | for key, values := range header {
334 | for _, value := range values {
335 | req.Header.Add(key, value)
336 | }
337 | }
338 | }
339 |
340 | // Sending
341 |
342 | // ResponseDecoder sets the Sling's response decoder.
343 | func (s *Sling) ResponseDecoder(decoder ResponseDecoder) *Sling {
344 | if decoder == nil {
345 | return s
346 | }
347 | s.responseDecoder = decoder
348 | return s
349 | }
350 |
351 | // ReceiveSuccess creates a new HTTP request and returns the response. Success
352 | // responses (2XX) are JSON decoded into the value pointed to by successV.
353 | // Any error creating the request, sending it, or decoding a 2XX response
354 | // is returned.
355 | func (s *Sling) ReceiveSuccess(successV interface{}) (*http.Response, error) {
356 | return s.Receive(successV, nil)
357 | }
358 |
359 | // Receive creates a new HTTP request and returns the response. Success
360 | // responses (2XX) are JSON decoded into the value pointed to by successV and
361 | // other responses are JSON decoded into the value pointed to by failureV.
362 | // If the status code of response is 204(no content) or the Content-Lenght is 0,
363 | // decoding is skipped. Any error creating the request, sending it, or decoding
364 | // the response is returned.
365 | // Receive is shorthand for calling Request and Do.
366 | func (s *Sling) Receive(successV, failureV interface{}) (*http.Response, error) {
367 | req, err := s.Request()
368 | if err != nil {
369 | return nil, err
370 | }
371 | return s.Do(req, successV, failureV)
372 | }
373 |
374 | // Do sends an HTTP request and returns the response. Success responses (2XX)
375 | // are JSON decoded into the value pointed to by successV and other responses
376 | // are JSON decoded into the value pointed to by failureV.
377 | // If the status code of response is 204(no content) or the Content-Length is 0,
378 | // decoding is skipped. Any error sending the request or decoding the response
379 | // is returned.
380 | func (s *Sling) Do(req *http.Request, successV, failureV interface{}) (*http.Response, error) {
381 | resp, err := s.httpClient.Do(req)
382 | if err != nil {
383 | return resp, err
384 | }
385 | // when err is nil, resp contains a non-nil resp.Body which must be closed
386 | defer resp.Body.Close()
387 |
388 | // The default HTTP client's Transport may not
389 | // reuse HTTP/1.x "keep-alive" TCP connections if the Body is
390 | // not read to completion and closed.
391 | // See: https://golang.org/pkg/net/http/#Response
392 | defer io.Copy(io.Discard, resp.Body)
393 |
394 | // Don't try to decode on 204s or Content-Length is 0
395 | if resp.StatusCode == http.StatusNoContent || resp.ContentLength == 0 {
396 | return resp, nil
397 | }
398 |
399 | // Decode from json
400 | if successV != nil || failureV != nil {
401 | err = decodeResponse(resp, s.responseDecoder, successV, failureV)
402 | }
403 | return resp, err
404 | }
405 |
406 | // decodeResponse decodes response Body into the value pointed to by successV
407 | // if the response is a success (2XX) or into the value pointed to by failureV
408 | // otherwise. If the successV or failureV argument to decode into is nil,
409 | // decoding is skipped.
410 | // Caller is responsible for closing the resp.Body.
411 | func decodeResponse(resp *http.Response, decoder ResponseDecoder, successV, failureV interface{}) error {
412 | if code := resp.StatusCode; 200 <= code && code <= 299 {
413 | if successV != nil {
414 | return decoder.Decode(resp, successV)
415 | }
416 | } else {
417 | if failureV != nil {
418 | return decoder.Decode(resp, failureV)
419 | }
420 | }
421 | return nil
422 | }
423 |
--------------------------------------------------------------------------------
/sling_test.go:
--------------------------------------------------------------------------------
1 | package sling
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/xml"
7 | "errors"
8 | "fmt"
9 | "io"
10 | "math"
11 | "net"
12 | "net/http"
13 | "net/http/httptest"
14 | "net/url"
15 | "reflect"
16 | "strings"
17 | "sync/atomic"
18 | "testing"
19 | )
20 |
21 | type FakeParams struct {
22 | KindName string `url:"kind_name"`
23 | Count int `url:"count"`
24 | }
25 |
26 | // Url-tagged query struct
27 | var paramsA = struct {
28 | Limit int `url:"limit"`
29 | }{
30 | 30,
31 | }
32 | var paramsB = FakeParams{KindName: "recent", Count: 25}
33 |
34 | // Json/XML-tagged model struct
35 | type FakeModel struct {
36 | Text string `json:"text,omitempty" xml:"text"`
37 | FavoriteCount int64 `json:"favorite_count,omitempty" xml:"favorite_count"`
38 | Temperature float64 `json:"temperature,omitempty" xml:"temperature"`
39 | }
40 |
41 | var modelA = FakeModel{Text: "note", FavoriteCount: 12}
42 |
43 | // Non-Json response decoder
44 | type xmlResponseDecoder struct{}
45 |
46 | func (d xmlResponseDecoder) Decode(resp *http.Response, v interface{}) error {
47 | return xml.NewDecoder(resp.Body).Decode(v)
48 | }
49 |
50 | func TestNew(t *testing.T) {
51 | sling := New()
52 | if sling.httpClient != http.DefaultClient {
53 | t.Errorf("expected %v, got %v", http.DefaultClient, sling.httpClient)
54 | }
55 | if sling.header == nil {
56 | t.Errorf("Header map not initialized with make")
57 | }
58 | if sling.queryStructs == nil {
59 | t.Errorf("queryStructs not initialized with make")
60 | }
61 | }
62 |
63 | func TestSlingNew(t *testing.T) {
64 | fakeBodyProvider := jsonBodyProvider{FakeModel{}}
65 |
66 | cases := []*Sling{
67 | &Sling{httpClient: &http.Client{}, method: "GET", rawURL: "http://example.com"},
68 | &Sling{httpClient: nil, method: "", rawURL: "http://example.com"},
69 | &Sling{queryStructs: make([]interface{}, 0)},
70 | &Sling{queryStructs: []interface{}{paramsA}},
71 | &Sling{queryStructs: []interface{}{paramsA, paramsB}},
72 | &Sling{bodyProvider: fakeBodyProvider},
73 | &Sling{bodyProvider: fakeBodyProvider},
74 | &Sling{bodyProvider: nil},
75 | New().Add("Content-Type", "application/json"),
76 | New().Add("A", "B").Add("a", "c").New(),
77 | New().Add("A", "B").New().Add("a", "c"),
78 | New().BodyForm(paramsB),
79 | New().BodyForm(paramsB).New(),
80 | }
81 | for _, sling := range cases {
82 | child := sling.New()
83 | if child.httpClient != sling.httpClient {
84 | t.Errorf("expected %v, got %v", sling.httpClient, child.httpClient)
85 | }
86 | if child.method != sling.method {
87 | t.Errorf("expected %s, got %s", sling.method, child.method)
88 | }
89 | if child.rawURL != sling.rawURL {
90 | t.Errorf("expected %s, got %s", sling.rawURL, child.rawURL)
91 | }
92 | // Header should be a copy of parent Sling header. For example, calling
93 | // baseSling.Add("k","v") should not mutate previously created child Slings
94 | if sling.header != nil {
95 | // struct literal cases don't init Header in usual way, skip header check
96 | if !reflect.DeepEqual(sling.header, child.header) {
97 | t.Errorf("not DeepEqual: expected %v, got %v", sling.header, child.header)
98 | }
99 | sling.header.Add("K", "V")
100 | if child.header.Get("K") != "" {
101 | t.Errorf("child.header was a reference to original map, should be copy")
102 | }
103 | }
104 | // queryStruct slice should be a new slice with a copy of the contents
105 | if len(sling.queryStructs) > 0 {
106 | // mutating one slice should not mutate the other
107 | child.queryStructs[0] = nil
108 | if sling.queryStructs[0] == nil {
109 | t.Errorf("child.queryStructs was a re-slice, expected slice with copied contents")
110 | }
111 | }
112 | // body should be copied
113 | if child.bodyProvider != sling.bodyProvider {
114 | t.Errorf("expected %v, got %v", sling.bodyProvider, child.bodyProvider)
115 | }
116 | }
117 | }
118 |
119 | func TestClientSetter(t *testing.T) {
120 | developerClient := &http.Client{}
121 | cases := []struct {
122 | input *http.Client
123 | expected *http.Client
124 | }{
125 | {nil, http.DefaultClient},
126 | {developerClient, developerClient},
127 | }
128 | for _, c := range cases {
129 | sling := New()
130 | sling.Client(c.input)
131 | if sling.httpClient != c.expected {
132 | t.Errorf("input %v, expected %v, got %v", c.input, c.expected, sling.httpClient)
133 | }
134 | }
135 | }
136 |
137 | func TestDoerSetter(t *testing.T) {
138 | developerClient := &http.Client{}
139 | cases := []struct {
140 | input Doer
141 | expected Doer
142 | }{
143 | {nil, http.DefaultClient},
144 | {developerClient, developerClient},
145 | }
146 | for _, c := range cases {
147 | sling := New()
148 | sling.Doer(c.input)
149 | if sling.httpClient != c.expected {
150 | t.Errorf("input %v, expected %v, got %v", c.input, c.expected, sling.httpClient)
151 | }
152 | }
153 | }
154 |
155 | func TestBaseSetter(t *testing.T) {
156 | cases := []string{"http://a.io/", "http://b.io", "/path", "path", ""}
157 | for _, base := range cases {
158 | sling := New().Base(base)
159 | if sling.rawURL != base {
160 | t.Errorf("expected %s, got %s", base, sling.rawURL)
161 | }
162 | }
163 | }
164 |
165 | func TestPathSetter(t *testing.T) {
166 | cases := []struct {
167 | rawURL string
168 | path string
169 | expectedRawURL string
170 | }{
171 | {"http://a.io/", "foo", "http://a.io/foo"},
172 | {"http://a.io/", "/foo", "http://a.io/foo"},
173 | {"http://a.io", "foo", "http://a.io/foo"},
174 | {"http://a.io", "/foo", "http://a.io/foo"},
175 | {"http://a.io/foo/", "bar", "http://a.io/foo/bar"},
176 | // rawURL should end in trailing slash if it is to be Path extended
177 | {"http://a.io/foo", "bar", "http://a.io/bar"},
178 | {"http://a.io/foo", "/bar", "http://a.io/bar"},
179 | // path extension is absolute
180 | {"http://a.io", "http://b.io/", "http://b.io/"},
181 | {"http://a.io/", "http://b.io/", "http://b.io/"},
182 | {"http://a.io", "http://b.io", "http://b.io"},
183 | {"http://a.io/", "http://b.io", "http://b.io"},
184 | // empty base, empty path
185 | {"", "http://b.io", "http://b.io"},
186 | {"http://a.io", "", "http://a.io"},
187 | {"", "", ""},
188 | }
189 | for _, c := range cases {
190 | sling := New().Base(c.rawURL).Path(c.path)
191 | if sling.rawURL != c.expectedRawURL {
192 | t.Errorf("expected %s, got %s", c.expectedRawURL, sling.rawURL)
193 | }
194 | }
195 | }
196 |
197 | func TestMethodSetters(t *testing.T) {
198 | cases := []struct {
199 | sling *Sling
200 | expectedMethod string
201 | }{
202 | {New().Path("http://a.io"), "GET"},
203 | {New().Head("http://a.io"), "HEAD"},
204 | {New().Get("http://a.io"), "GET"},
205 | {New().Post("http://a.io"), "POST"},
206 | {New().Put("http://a.io"), "PUT"},
207 | {New().Patch("http://a.io"), "PATCH"},
208 | {New().Delete("http://a.io"), "DELETE"},
209 | {New().Options("http://a.io"), "OPTIONS"},
210 | {New().Trace("http://a.io"), "TRACE"},
211 | {New().Connect("http://a.io"), "CONNECT"},
212 | }
213 | for _, c := range cases {
214 | if c.sling.method != c.expectedMethod {
215 | t.Errorf("expected method %s, got %s", c.expectedMethod, c.sling.method)
216 | }
217 | }
218 | }
219 |
220 | func TestAddHeader(t *testing.T) {
221 | cases := []struct {
222 | sling *Sling
223 | expectedHeader map[string][]string
224 | }{
225 | {New().Add("authorization", "OAuth key=\"value\""), map[string][]string{"Authorization": []string{"OAuth key=\"value\""}}},
226 | // header keys should be canonicalized
227 | {New().Add("content-tYPE", "application/json").Add("User-AGENT", "sling"), map[string][]string{"Content-Type": []string{"application/json"}, "User-Agent": []string{"sling"}}},
228 | // values for existing keys should be appended
229 | {New().Add("A", "B").Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}},
230 | // Add should add to values for keys added by parent Slings
231 | {New().Add("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}},
232 | {New().Add("A", "B").New().Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}},
233 | }
234 | for _, c := range cases {
235 | // type conversion from header to alias'd map for deep equality comparison
236 | headerMap := map[string][]string(c.sling.header)
237 | if !reflect.DeepEqual(c.expectedHeader, headerMap) {
238 | t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap)
239 | }
240 | }
241 | }
242 |
243 | func TestSetHeader(t *testing.T) {
244 | cases := []struct {
245 | sling *Sling
246 | expectedHeader map[string][]string
247 | }{
248 | // should replace existing values associated with key
249 | {New().Add("A", "B").Set("a", "c"), map[string][]string{"A": []string{"c"}}},
250 | {New().Set("content-type", "A").Set("Content-Type", "B"), map[string][]string{"Content-Type": []string{"B"}}},
251 | // Set should replace values received by copying parent Slings
252 | {New().Set("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}},
253 | {New().Add("A", "B").New().Set("a", "c"), map[string][]string{"A": []string{"c"}}},
254 | }
255 | for _, c := range cases {
256 | // type conversion from Header to alias'd map for deep equality comparison
257 | headerMap := map[string][]string(c.sling.header)
258 | if !reflect.DeepEqual(c.expectedHeader, headerMap) {
259 | t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap)
260 | }
261 | }
262 | }
263 |
264 | func TestBasicAuth(t *testing.T) {
265 | cases := []struct {
266 | sling *Sling
267 | expectedAuth []string
268 | }{
269 | // basic auth: username & password
270 | {New().SetBasicAuth("Aladdin", "open sesame"), []string{"Aladdin", "open sesame"}},
271 | // empty username
272 | {New().SetBasicAuth("", "secret"), []string{"", "secret"}},
273 | // empty password
274 | {New().SetBasicAuth("admin", ""), []string{"admin", ""}},
275 | }
276 | for _, c := range cases {
277 | req, err := c.sling.Request()
278 | if err != nil {
279 | t.Errorf("unexpected error when building Request with .SetBasicAuth()")
280 | }
281 | username, password, ok := req.BasicAuth()
282 | if !ok {
283 | t.Errorf("basic auth missing when expected")
284 | }
285 | auth := []string{username, password}
286 | if !reflect.DeepEqual(c.expectedAuth, auth) {
287 | t.Errorf("not DeepEqual: expected %v, got %v", c.expectedAuth, auth)
288 | }
289 | }
290 | }
291 |
292 | func TestQueryStructSetter(t *testing.T) {
293 | cases := []struct {
294 | sling *Sling
295 | expectedStructs []interface{}
296 | }{
297 | {New(), []interface{}{}},
298 | {New().QueryStruct(nil), []interface{}{}},
299 | {New().QueryStruct(paramsA), []interface{}{paramsA}},
300 | {New().QueryStruct(paramsA).QueryStruct(paramsA), []interface{}{paramsA, paramsA}},
301 | {New().QueryStruct(paramsA).QueryStruct(paramsB), []interface{}{paramsA, paramsB}},
302 | {New().QueryStruct(paramsA).New(), []interface{}{paramsA}},
303 | {New().QueryStruct(paramsA).New().QueryStruct(paramsB), []interface{}{paramsA, paramsB}},
304 | }
305 |
306 | for _, c := range cases {
307 | if count := len(c.sling.queryStructs); count != len(c.expectedStructs) {
308 | t.Errorf("expected length %d, got %d", len(c.expectedStructs), count)
309 | }
310 | check:
311 | for _, expected := range c.expectedStructs {
312 | for _, param := range c.sling.queryStructs {
313 | if param == expected {
314 | continue check
315 | }
316 | }
317 | t.Errorf("expected to find %v in %v", expected, c.sling.queryStructs)
318 | }
319 | }
320 | }
321 |
322 | func TestBodyJSONSetter(t *testing.T) {
323 | fakeModel := &FakeModel{}
324 | fakeBodyProvider := jsonBodyProvider{payload: fakeModel}
325 |
326 | cases := []struct {
327 | initial BodyProvider
328 | input interface{}
329 | expected BodyProvider
330 | }{
331 | // json tagged struct is set as bodyJSON
332 | {nil, fakeModel, fakeBodyProvider},
333 | // nil argument to bodyJSON does not replace existing bodyJSON
334 | {fakeBodyProvider, nil, fakeBodyProvider},
335 | // nil bodyJSON remains nil
336 | {nil, nil, nil},
337 | }
338 | for _, c := range cases {
339 | sling := New()
340 | sling.bodyProvider = c.initial
341 | sling.BodyJSON(c.input)
342 | if sling.bodyProvider != c.expected {
343 | t.Errorf("expected %v, got %v", c.expected, sling.bodyProvider)
344 | }
345 | // Header Content-Type should be application/json if bodyJSON arg was non-nil
346 | if c.input != nil && sling.header.Get(contentType) != jsonContentType {
347 | t.Errorf("Incorrect or missing header, expected %s, got %s", jsonContentType, sling.header.Get(contentType))
348 | } else if c.input == nil && sling.header.Get(contentType) != "" {
349 | t.Errorf("did not expect a Content-Type header, got %s", sling.header.Get(contentType))
350 | }
351 | }
352 | }
353 |
354 | func TestBodyFormSetter(t *testing.T) {
355 | fakeParams := FakeParams{KindName: "recent", Count: 25}
356 | fakeBodyProvider := formBodyProvider{payload: fakeParams}
357 |
358 | cases := []struct {
359 | initial BodyProvider
360 | input interface{}
361 | expected BodyProvider
362 | }{
363 | // url tagged struct is set as bodyStruct
364 | {nil, paramsB, fakeBodyProvider},
365 | // nil argument to bodyStruct does not replace existing bodyStruct
366 | {fakeBodyProvider, nil, fakeBodyProvider},
367 | // nil bodyStruct remains nil
368 | {nil, nil, nil},
369 | }
370 | for _, c := range cases {
371 | sling := New()
372 | sling.bodyProvider = c.initial
373 | sling.BodyForm(c.input)
374 | if sling.bodyProvider != c.expected {
375 | t.Errorf("expected %v, got %v", c.expected, sling.bodyProvider)
376 | }
377 | // Content-Type should be application/x-www-form-urlencoded if bodyStruct was non-nil
378 | if c.input != nil && sling.header.Get(contentType) != formContentType {
379 | t.Errorf("Incorrect or missing header, expected %s, got %s", formContentType, sling.header.Get(contentType))
380 | } else if c.input == nil && sling.header.Get(contentType) != "" {
381 | t.Errorf("did not expect a Content-Type header, got %s", sling.header.Get(contentType))
382 | }
383 | }
384 | }
385 |
386 | func TestBodySetter(t *testing.T) {
387 | fakeInput := io.NopCloser(strings.NewReader("test"))
388 | fakeBodyProvider := bodyProvider{body: fakeInput}
389 |
390 | cases := []struct {
391 | initial BodyProvider
392 | input io.Reader
393 | expected BodyProvider
394 | }{
395 | // nil body is overriden by a set body
396 | {nil, fakeInput, fakeBodyProvider},
397 | // initial body is not overriden by nil body
398 | {fakeBodyProvider, nil, fakeBodyProvider},
399 | // nil body is returned unaltered
400 | {nil, nil, nil},
401 | }
402 | for _, c := range cases {
403 | sling := New()
404 | sling.bodyProvider = c.initial
405 | sling.Body(c.input)
406 | if sling.bodyProvider != c.expected {
407 | t.Errorf("expected %v, got %v", c.expected, sling.bodyProvider)
408 | }
409 | }
410 | }
411 |
412 | func TestRequest_urlAndMethod(t *testing.T) {
413 | cases := []struct {
414 | sling *Sling
415 | expectedMethod string
416 | expectedURL string
417 | expectedErr error
418 | }{
419 | {New().Base("http://a.io"), "GET", "http://a.io", nil},
420 | {New().Path("http://a.io"), "GET", "http://a.io", nil},
421 | {New().Get("http://a.io"), "GET", "http://a.io", nil},
422 | {New().Put("http://a.io"), "PUT", "http://a.io", nil},
423 | {New().Base("http://a.io/").Path("foo"), "GET", "http://a.io/foo", nil},
424 | {New().Base("http://a.io/").Post("foo"), "POST", "http://a.io/foo", nil},
425 | // if relative path is an absolute url, base is ignored
426 | {New().Base("http://a.io").Path("http://b.io"), "GET", "http://b.io", nil},
427 | {New().Path("http://a.io").Path("http://b.io"), "GET", "http://b.io", nil},
428 | // last method setter takes priority
429 | {New().Get("http://b.io").Post("http://a.io"), "POST", "http://a.io", nil},
430 | {New().Post("http://a.io/").Put("foo/").Delete("bar"), "DELETE", "http://a.io/foo/bar", nil},
431 | // last Base setter takes priority
432 | {New().Base("http://a.io").Base("http://b.io"), "GET", "http://b.io", nil},
433 | // Path setters are additive
434 | {New().Base("http://a.io/").Path("foo/").Path("bar"), "GET", "http://a.io/foo/bar", nil},
435 | {New().Path("http://a.io/").Path("foo/").Path("bar"), "GET", "http://a.io/foo/bar", nil},
436 | // removes extra '/' between base and ref url
437 | {New().Base("http://a.io/").Get("/foo"), "GET", "http://a.io/foo", nil},
438 | }
439 | for _, c := range cases {
440 | req, err := c.sling.Request()
441 | if err != c.expectedErr {
442 | t.Errorf("expected error %v, got %v for %+v", c.expectedErr, err, c.sling)
443 | }
444 | if req.URL.String() != c.expectedURL {
445 | t.Errorf("expected url %s, got %s for %+v", c.expectedURL, req.URL.String(), c.sling)
446 | }
447 | if req.Method != c.expectedMethod {
448 | t.Errorf("expected method %s, got %s for %+v", c.expectedMethod, req.Method, c.sling)
449 | }
450 | }
451 | }
452 |
453 | func TestRequest_queryStructs(t *testing.T) {
454 | cases := []struct {
455 | sling *Sling
456 | expectedURL string
457 | }{
458 | {New().Base("http://a.io").QueryStruct(paramsA), "http://a.io?limit=30"},
459 | {New().Base("http://a.io").QueryStruct(paramsA).QueryStruct(paramsB), "http://a.io?count=25&kind_name=recent&limit=30"},
460 | {New().Base("http://a.io/").Path("foo?path=yes").QueryStruct(paramsA), "http://a.io/foo?limit=30&path=yes"},
461 | {New().Base("http://a.io").QueryStruct(paramsA).New(), "http://a.io?limit=30"},
462 | {New().Base("http://a.io").QueryStruct(paramsA).New().QueryStruct(paramsB), "http://a.io?count=25&kind_name=recent&limit=30"},
463 | }
464 | for _, c := range cases {
465 | req, _ := c.sling.Request()
466 | if req.URL.String() != c.expectedURL {
467 | t.Errorf("expected url %s, got %s for %+v", c.expectedURL, req.URL.String(), c.sling)
468 | }
469 | }
470 | }
471 |
472 | func TestRequest_body(t *testing.T) {
473 | cases := []struct {
474 | sling *Sling
475 | expectedBody string // expected Body io.Reader as a string
476 | expectedContentType string
477 | }{
478 | // BodyJSON
479 | {New().BodyJSON(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
480 | {New().BodyJSON(&modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
481 | {New().BodyJSON(&FakeModel{}), "{}\n", jsonContentType},
482 | {New().BodyJSON(FakeModel{}), "{}\n", jsonContentType},
483 | // BodyJSON overrides existing values
484 | {New().BodyJSON(&FakeModel{}).BodyJSON(&FakeModel{Text: "msg"}), "{\"text\":\"msg\"}\n", jsonContentType},
485 | // BodyForm
486 | {New().BodyForm(paramsA), "limit=30", formContentType},
487 | {New().BodyForm(paramsB), "count=25&kind_name=recent", formContentType},
488 | {New().BodyForm(¶msB), "count=25&kind_name=recent", formContentType},
489 | // BodyForm overrides existing values
490 | {New().BodyForm(paramsA).New().BodyForm(paramsB), "count=25&kind_name=recent", formContentType},
491 | // Mixture of BodyJSON and BodyForm prefers body setter called last with a non-nil argument
492 | {New().BodyForm(paramsB).New().BodyJSON(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
493 | {New().BodyJSON(modelA).New().BodyForm(paramsB), "count=25&kind_name=recent", formContentType},
494 | {New().BodyForm(paramsB).New().BodyJSON(nil), "count=25&kind_name=recent", formContentType},
495 | {New().BodyJSON(modelA).New().BodyForm(nil), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
496 | // Body
497 | {New().Body(strings.NewReader("this-is-a-test")), "this-is-a-test", ""},
498 | {New().Body(strings.NewReader("a")).Body(strings.NewReader("b")), "b", ""},
499 | }
500 | for _, c := range cases {
501 | req, _ := c.sling.Request()
502 | buf := new(bytes.Buffer)
503 | buf.ReadFrom(req.Body)
504 | // req.Body should have contained the expectedBody string
505 | if value := buf.String(); value != c.expectedBody {
506 | t.Errorf("expected Request.Body %s, got %s", c.expectedBody, value)
507 | }
508 | // Header Content-Type should be expectedContentType ("" means no contentType expected)
509 | if actualHeader := req.Header.Get(contentType); actualHeader != c.expectedContentType && c.expectedContentType != "" {
510 | t.Errorf("Incorrect or missing header, expected %s, got %s", c.expectedContentType, actualHeader)
511 | }
512 | }
513 | }
514 |
515 | func TestRequest_bodyNoData(t *testing.T) {
516 | // test that Body is left nil when no bodyJSON or bodyStruct set
517 | slings := []*Sling{
518 | New(),
519 | New().BodyJSON(nil),
520 | New().BodyForm(nil),
521 | }
522 | for _, sling := range slings {
523 | req, _ := sling.Request()
524 | if req.Body != nil {
525 | t.Errorf("expected nil Request.Body, got %v", req.Body)
526 | }
527 | // Header Content-Type should not be set when bodyJSON argument was nil or never called
528 | if actualHeader := req.Header.Get(contentType); actualHeader != "" {
529 | t.Errorf("did not expect a Content-Type header, got %s", actualHeader)
530 | }
531 | }
532 | }
533 |
534 | func TestRequest_bodyEncodeErrors(t *testing.T) {
535 | cases := []struct {
536 | sling *Sling
537 | expectedErr error
538 | }{
539 | // check that Encode errors are propagated, illegal JSON field
540 | {New().BodyJSON(FakeModel{Temperature: math.Inf(1)}), errors.New("json: unsupported value: +Inf")},
541 | }
542 | for _, c := range cases {
543 | req, err := c.sling.Request()
544 | if err == nil || err.Error() != c.expectedErr.Error() {
545 | t.Errorf("expected error %v, got %v", c.expectedErr, err)
546 | }
547 | if req != nil {
548 | t.Errorf("expected nil Request, got %+v", req)
549 | }
550 | }
551 | }
552 |
553 | func TestRequest_headers(t *testing.T) {
554 | cases := []struct {
555 | sling *Sling
556 | expectedHeader map[string][]string
557 | }{
558 | {New().Add("authorization", "OAuth key=\"value\""), map[string][]string{"Authorization": []string{"OAuth key=\"value\""}}},
559 | // header keys should be canonicalized
560 | {New().Add("content-tYPE", "application/json").Add("User-AGENT", "sling"), map[string][]string{"Content-Type": []string{"application/json"}, "User-Agent": []string{"sling"}}},
561 | // values for existing keys should be appended
562 | {New().Add("A", "B").Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}},
563 | // Add should add to values for keys added by parent Slings
564 | {New().Add("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}},
565 | {New().Add("A", "B").New().Add("a", "c"), map[string][]string{"A": []string{"B", "c"}}},
566 | // Add and Set
567 | {New().Add("A", "B").Set("a", "c"), map[string][]string{"A": []string{"c"}}},
568 | {New().Set("content-type", "A").Set("Content-Type", "B"), map[string][]string{"Content-Type": []string{"B"}}},
569 | // Set should replace values received by copying parent Slings
570 | {New().Set("A", "B").Add("a", "c").New(), map[string][]string{"A": []string{"B", "c"}}},
571 | {New().Add("A", "B").New().Set("a", "c"), map[string][]string{"A": []string{"c"}}},
572 | }
573 | for _, c := range cases {
574 | req, _ := c.sling.Request()
575 | // type conversion from Header to alias'd map for deep equality comparison
576 | headerMap := map[string][]string(req.Header)
577 | if !reflect.DeepEqual(c.expectedHeader, headerMap) {
578 | t.Errorf("not DeepEqual: expected %v, got %v", c.expectedHeader, headerMap)
579 | }
580 | }
581 | }
582 |
583 | func TestAddQueryStructs(t *testing.T) {
584 | cases := []struct {
585 | rawurl string
586 | queryStructs []interface{}
587 | expected string
588 | }{
589 | {"http://a.io", []interface{}{}, "http://a.io"},
590 | {"http://a.io", []interface{}{paramsA}, "http://a.io?limit=30"},
591 | {"http://a.io", []interface{}{paramsA, paramsA}, "http://a.io?limit=30&limit=30"},
592 | {"http://a.io", []interface{}{paramsA, paramsB}, "http://a.io?count=25&kind_name=recent&limit=30"},
593 | // don't blow away query values on the rawURL (parsed into RawQuery)
594 | {"http://a.io?initial=7", []interface{}{paramsA}, "http://a.io?initial=7&limit=30"},
595 | }
596 | for _, c := range cases {
597 | reqURL, _ := url.Parse(c.rawurl)
598 | addQueryStructs(reqURL, c.queryStructs)
599 | if reqURL.String() != c.expected {
600 | t.Errorf("expected %s, got %s", c.expected, reqURL.String())
601 | }
602 | }
603 | }
604 |
605 | // Sending
606 |
607 | type APIError struct {
608 | Message string `json:"message"`
609 | Code int `json:"code"`
610 | }
611 |
612 | func TestDo_onSuccess(t *testing.T) {
613 | const expectedText = "Some text"
614 | const expectedFavoriteCount int64 = 24
615 |
616 | client, mux, server := testServer()
617 | defer server.Close()
618 | mux.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) {
619 | w.Header().Set("Content-Type", "application/json")
620 | fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`)
621 | })
622 |
623 | sling := New().Client(client)
624 | req, _ := http.NewRequest("GET", "http://example.com/success", nil)
625 |
626 | model := new(FakeModel)
627 | apiError := new(APIError)
628 | resp, err := sling.Do(req, model, apiError)
629 |
630 | if err != nil {
631 | t.Errorf("expected nil, got %v", err)
632 | }
633 | if resp.StatusCode != 200 {
634 | t.Errorf("expected %d, got %d", 200, resp.StatusCode)
635 | }
636 | if model.Text != expectedText {
637 | t.Errorf("expected %s, got %s", expectedText, model.Text)
638 | }
639 | if model.FavoriteCount != expectedFavoriteCount {
640 | t.Errorf("expected %d, got %d", expectedFavoriteCount, model.FavoriteCount)
641 | }
642 | }
643 |
644 | func TestDo_onSuccessWithNilValue(t *testing.T) {
645 | client, mux, server := testServer()
646 | defer server.Close()
647 | mux.HandleFunc("/success", func(w http.ResponseWriter, r *http.Request) {
648 | w.Header().Set("Content-Type", "application/json")
649 | fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`)
650 | })
651 |
652 | sling := New().Client(client)
653 | req, _ := http.NewRequest("GET", "http://example.com/success", nil)
654 |
655 | apiError := new(APIError)
656 | resp, err := sling.Do(req, nil, apiError)
657 |
658 | if err != nil {
659 | t.Errorf("expected nil, got %v", err)
660 | }
661 | if resp.StatusCode != 200 {
662 | t.Errorf("expected %d, got %d", 200, resp.StatusCode)
663 | }
664 | expected := &APIError{}
665 | if !reflect.DeepEqual(expected, apiError) {
666 | t.Errorf("failureV should not be populated, exepcted %v, got %v", expected, apiError)
667 | }
668 | }
669 |
670 | func TestDo_noContent(t *testing.T) {
671 | client, mux, server := testServer()
672 | defer server.Close()
673 | mux.HandleFunc("/nocontent", func(w http.ResponseWriter, r *http.Request) {
674 | w.WriteHeader(204)
675 | })
676 |
677 | sling := New().Client(client)
678 | req, _ := http.NewRequest("DELETE", "http://example.com/nocontent", nil)
679 |
680 | model := new(FakeModel)
681 | apiError := new(APIError)
682 | resp, err := sling.Do(req, model, apiError)
683 |
684 | if err != nil {
685 | t.Errorf("expected nil, got %v", err)
686 | }
687 | if resp.StatusCode != 204 {
688 | t.Errorf("expected %d, got %d", 204, resp.StatusCode)
689 | }
690 | expectedModel := &FakeModel{}
691 | if !reflect.DeepEqual(expectedModel, model) {
692 | t.Errorf("successV should not be populated, exepcted %v, got %v", expectedModel, model)
693 | }
694 | expectedAPIError := &APIError{}
695 | if !reflect.DeepEqual(expectedAPIError, apiError) {
696 | t.Errorf("failureV should not be populated, exepcted %v, got %v", expectedAPIError, apiError)
697 | }
698 | }
699 |
700 | func TestDo_onFailure(t *testing.T) {
701 | const expectedMessage = "Invalid argument"
702 | const expectedCode int = 215
703 |
704 | client, mux, server := testServer()
705 | defer server.Close()
706 | mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) {
707 | w.Header().Set("Content-Type", "application/json")
708 | w.WriteHeader(400)
709 | fmt.Fprintf(w, `{"message": "Invalid argument", "code": 215}`)
710 | })
711 |
712 | sling := New().Client(client)
713 | req, _ := http.NewRequest("GET", "http://example.com/failure", nil)
714 |
715 | model := new(FakeModel)
716 | apiError := new(APIError)
717 | resp, err := sling.Do(req, model, apiError)
718 |
719 | if err != nil {
720 | t.Errorf("expected nil, got %v", err)
721 | }
722 | if resp.StatusCode != 400 {
723 | t.Errorf("expected %d, got %d", 400, resp.StatusCode)
724 | }
725 | if apiError.Message != expectedMessage {
726 | t.Errorf("expected %s, got %s", expectedMessage, apiError.Message)
727 | }
728 | if apiError.Code != expectedCode {
729 | t.Errorf("expected %d, got %d", expectedCode, apiError.Code)
730 | }
731 | }
732 |
733 | func TestDo_onFailureWithNilValue(t *testing.T) {
734 | client, mux, server := testServer()
735 | defer server.Close()
736 | mux.HandleFunc("/failure", func(w http.ResponseWriter, r *http.Request) {
737 | w.Header().Set("Content-Type", "application/json")
738 | w.WriteHeader(420)
739 | fmt.Fprintf(w, `{"message": "Enhance your calm", "code": 88}`)
740 | })
741 |
742 | sling := New().Client(client)
743 | req, _ := http.NewRequest("GET", "http://example.com/failure", nil)
744 |
745 | model := new(FakeModel)
746 | resp, err := sling.Do(req, model, nil)
747 |
748 | if err != nil {
749 | t.Errorf("expected nil, got %v", err)
750 | }
751 | if resp.StatusCode != 420 {
752 | t.Errorf("expected %d, got %d", 420, resp.StatusCode)
753 | }
754 | expected := &FakeModel{}
755 | if !reflect.DeepEqual(expected, model) {
756 | t.Errorf("successV should not be populated, exepcted %v, got %v", expected, model)
757 | }
758 | }
759 |
760 | func TestReceive_success_nonDefaultDecoder(t *testing.T) {
761 | client, mux, server := testServer()
762 | defer server.Close()
763 | mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) {
764 | w.Header().Set("Content-Type", "application/xml")
765 | data := `
766 | Some text
767 | 24
768 | 10.5
769 | `
770 | fmt.Fprintf(w, xml.Header)
771 | fmt.Fprint(w, data)
772 | })
773 |
774 | endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Post("submit")
775 |
776 | model := new(FakeModel)
777 | apiError := new(APIError)
778 | resp, err := endpoint.New().ResponseDecoder(xmlResponseDecoder{}).Receive(model, apiError)
779 |
780 | if err != nil {
781 | t.Errorf("expected nil, got %v", err)
782 | }
783 | if resp.StatusCode != 200 {
784 | t.Errorf("expected %d, got %d", 200, resp.StatusCode)
785 | }
786 | expectedModel := &FakeModel{Text: "Some text", FavoriteCount: 24, Temperature: 10.5}
787 | if !reflect.DeepEqual(expectedModel, model) {
788 | t.Errorf("expected %v, got %v", expectedModel, model)
789 | }
790 | expectedAPIError := &APIError{}
791 | if !reflect.DeepEqual(expectedAPIError, apiError) {
792 | t.Errorf("failureV should be zero valued, exepcted %v, got %v", expectedAPIError, apiError)
793 | }
794 | }
795 |
796 | func TestReceive_success(t *testing.T) {
797 | client, mux, server := testServer()
798 | defer server.Close()
799 | mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) {
800 | assertMethod(t, "POST", r)
801 | assertQuery(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r)
802 | assertPostForm(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r)
803 | w.Header().Set("Content-Type", "application/json")
804 | fmt.Fprintf(w, `{"text": "Some text", "favorite_count": 24}`)
805 | })
806 |
807 | endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Post("submit")
808 | // encode url-tagged struct in query params and as post body for testing purposes
809 | params := FakeParams{KindName: "vanilla", Count: 11}
810 | model := new(FakeModel)
811 | apiError := new(APIError)
812 | resp, err := endpoint.New().QueryStruct(params).BodyForm(params).Receive(model, apiError)
813 |
814 | if err != nil {
815 | t.Errorf("expected nil, got %v", err)
816 | }
817 | if resp.StatusCode != 200 {
818 | t.Errorf("expected %d, got %d", 200, resp.StatusCode)
819 | }
820 | expectedModel := &FakeModel{Text: "Some text", FavoriteCount: 24}
821 | if !reflect.DeepEqual(expectedModel, model) {
822 | t.Errorf("expected %v, got %v", expectedModel, model)
823 | }
824 | expectedAPIError := &APIError{}
825 | if !reflect.DeepEqual(expectedAPIError, apiError) {
826 | t.Errorf("failureV should be zero valued, exepcted %v, got %v", expectedAPIError, apiError)
827 | }
828 | }
829 |
830 | func TestReceive_StatusOKNoContent(t *testing.T) {
831 | client, mux, server := testServer()
832 | defer server.Close()
833 | mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) {
834 | assertMethod(t, "POST", r)
835 | w.WriteHeader(201)
836 | w.Header().Set("Location", "/foo/latest")
837 | })
838 |
839 | endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Post("submit")
840 | // fake a post response for testing purposes, checking that it's valid happens in other tests
841 | params := FakeParams{}
842 | model := new(FakeModel)
843 | apiError := new(APIError)
844 | resp, err := endpoint.New().BodyForm(params).Receive(model, apiError)
845 |
846 | if err != nil {
847 | t.Errorf("expected nil, got %v", err)
848 | }
849 | if resp.StatusCode != 201 {
850 | t.Errorf("expected %d, got %d", 201, resp.StatusCode)
851 | }
852 | expectedModel := &FakeModel{}
853 | if !reflect.DeepEqual(expectedModel, model) {
854 | t.Errorf("expected %v, got %v", expectedModel, model)
855 | }
856 | expectedAPIError := &APIError{}
857 | if !reflect.DeepEqual(expectedAPIError, apiError) {
858 | t.Errorf("failureV should be zero valued, exepcted %v, got %v", expectedAPIError, apiError)
859 | }
860 | }
861 |
862 | func TestReceive_failure(t *testing.T) {
863 | client, mux, server := testServer()
864 | defer server.Close()
865 | mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) {
866 | assertMethod(t, "POST", r)
867 | assertQuery(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r)
868 | assertPostForm(t, map[string]string{"kind_name": "vanilla", "count": "11"}, r)
869 | w.Header().Set("Content-Type", "application/json")
870 | w.WriteHeader(429)
871 | fmt.Fprintf(w, `{"message": "Rate limit exceeded", "code": 88}`)
872 | })
873 |
874 | endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Post("submit")
875 | // encode url-tagged struct in query params and as post body for testing purposes
876 | params := FakeParams{KindName: "vanilla", Count: 11}
877 | model := new(FakeModel)
878 | apiError := new(APIError)
879 | resp, err := endpoint.New().QueryStruct(params).BodyForm(params).Receive(model, apiError)
880 |
881 | if err != nil {
882 | t.Errorf("expected nil, got %v", err)
883 | }
884 | if resp.StatusCode != 429 {
885 | t.Errorf("expected %d, got %d", 429, resp.StatusCode)
886 | }
887 | expectedAPIError := &APIError{Message: "Rate limit exceeded", Code: 88}
888 | if !reflect.DeepEqual(expectedAPIError, apiError) {
889 | t.Errorf("expected %v, got %v", expectedAPIError, apiError)
890 | }
891 | expectedModel := &FakeModel{}
892 | if !reflect.DeepEqual(expectedModel, model) {
893 | t.Errorf("successV should not be zero valued, expected %v, got %v", expectedModel, model)
894 | }
895 | }
896 |
897 | func TestReceive_noContent(t *testing.T) {
898 | client, mux, server := testServer()
899 | defer server.Close()
900 | mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) {
901 | assertMethod(t, "HEAD", r)
902 | w.WriteHeader(204)
903 | })
904 |
905 | endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Head("submit")
906 | resp, err := endpoint.New().Receive(nil, nil)
907 |
908 | if err != nil {
909 | t.Errorf("expected nil, got %v", err)
910 | }
911 | if resp.StatusCode != 204 {
912 | t.Errorf("expected %d, got %d", 204, resp.StatusCode)
913 | }
914 | }
915 |
916 | func TestReceive_errorCreatingRequest(t *testing.T) {
917 | expectedErr := errors.New("json: unsupported value: +Inf")
918 | resp, err := New().BodyJSON(FakeModel{Temperature: math.Inf(1)}).Receive(nil, nil)
919 | if err == nil || err.Error() != expectedErr.Error() {
920 | t.Errorf("expected %v, got %v", expectedErr, err)
921 | }
922 | if resp != nil {
923 | t.Errorf("expected nil resp, got %v", resp)
924 | }
925 | }
926 |
927 | func TestReuseTcpConnections(t *testing.T) {
928 | var connCount int32
929 |
930 | ln, _ := net.Listen("tcp", ":0")
931 | rawURL := fmt.Sprintf("http://%s/", ln.Addr())
932 |
933 | server := http.Server{
934 | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
935 | assertMethod(t, "GET", r)
936 | fmt.Fprintf(w, `{"text": "Some text"}`)
937 | }),
938 | ConnState: func(conn net.Conn, state http.ConnState) {
939 | if state == http.StateNew {
940 | atomic.AddInt32(&connCount, 1)
941 | }
942 | },
943 | }
944 |
945 | go server.Serve(ln)
946 |
947 | endpoint := New().Client(http.DefaultClient).Base(rawURL).Path("foo/").Get("get")
948 |
949 | for i := 0; i < 10; i++ {
950 | resp, err := endpoint.New().Receive(nil, nil)
951 | if err != nil {
952 | t.Errorf("expected nil, got %v", err)
953 | }
954 | if resp.StatusCode != 200 {
955 | t.Errorf("expected %d, got %d", 200, resp.StatusCode)
956 | }
957 | }
958 |
959 | server.Shutdown(context.Background())
960 |
961 | if count := atomic.LoadInt32(&connCount); count != 1 {
962 | t.Errorf("expected 1, got %v", count)
963 | }
964 | }
965 |
966 | // Testing Utils
967 |
968 | // testServer returns an http Client, ServeMux, and Server. The client proxies
969 | // requests to the server and handlers can be registered on the mux to handle
970 | // requests. The caller must close the test server.
971 | func testServer() (*http.Client, *http.ServeMux, *httptest.Server) {
972 | mux := http.NewServeMux()
973 | server := httptest.NewServer(mux)
974 | transport := &http.Transport{
975 | Proxy: func(req *http.Request) (*url.URL, error) {
976 | return url.Parse(server.URL)
977 | },
978 | }
979 | client := &http.Client{Transport: transport}
980 | return client, mux, server
981 | }
982 |
983 | func assertMethod(t *testing.T, expectedMethod string, req *http.Request) {
984 | if actualMethod := req.Method; actualMethod != expectedMethod {
985 | t.Errorf("expected method %s, got %s", expectedMethod, actualMethod)
986 | }
987 | }
988 |
989 | // assertQuery tests that the Request has the expected url query key/val pairs
990 | func assertQuery(t *testing.T, expected map[string]string, req *http.Request) {
991 | queryValues := req.URL.Query() // net/url Values is a map[string][]string
992 | expectedValues := url.Values{}
993 | for key, value := range expected {
994 | expectedValues.Add(key, value)
995 | }
996 | if !reflect.DeepEqual(expectedValues, queryValues) {
997 | t.Errorf("expected parameters %v, got %v", expected, req.URL.RawQuery)
998 | }
999 | }
1000 |
1001 | // assertPostForm tests that the Request has the expected key values pairs url
1002 | // encoded in its Body
1003 | func assertPostForm(t *testing.T, expected map[string]string, req *http.Request) {
1004 | req.ParseForm() // parses request Body to put url.Values in r.Form/r.PostForm
1005 | expectedValues := url.Values{}
1006 | for key, value := range expected {
1007 | expectedValues.Add(key, value)
1008 | }
1009 | if !reflect.DeepEqual(expectedValues, req.PostForm) {
1010 | t.Errorf("expected parameters %v, got %v", expected, req.PostForm)
1011 | }
1012 | }
1013 |
--------------------------------------------------------------------------------