295 lines
9.6 KiB
Markdown
295 lines
9.6 KiB
Markdown
# Sling [![Build Status](https://github.com/dghubble/sling/workflows/test/badge.svg)](https://github.com/dghubble/oauth1/actions?query=workflow%3Atest+branch%3Amaster) [![Coverage](https://gocover.io/_badge/github.com/dghubble/sling)](https://gocover.io/github.com/dghubble/sling) [![GoDoc](https://godoc.org/github.com/dghubble/sling?status.svg)](https://godoc.org/github.com/dghubble/sling)
|
|
|
|
<img align="right" src="https://storage.googleapis.com/dghubble/small-gopher-with-sling.png">
|
|
|
|
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`, `Head`, `Options`, `Trace`, or `Connect` 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.
|
|
|
|
### Modify a Request
|
|
|
|
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:
|
|
|
|
```go
|
|
req, err := sling.New().Get("https://example.com").QueryStruct(params).Request()
|
|
// handle error
|
|
|
|
trace := &httptrace.ClientTrace{
|
|
DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
|
|
fmt.Printf("DNS Info: %+v\n", dnsInfo)
|
|
},
|
|
GotConn: func(connInfo httptrace.GotConnInfo) {
|
|
fmt.Printf("Got Conn: %+v\n", connInfo)
|
|
},
|
|
}
|
|
|
|
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
|
|
client.Do(req)
|
|
```
|
|
|
|
### 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)
|
|
* Swagger Generator [swagger-api/swagger-codegen](https://github.com/swagger-api/swagger-codegen)
|
|
* Twitter [dghubble/go-twitter](https://github.com/dghubble/go-twitter)
|
|
* 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)
|