Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposed v0.2 #7

Merged
merged 11 commits into from
May 23, 2015
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ go:
- tip
before_install:
- go get golang.org/x/tools/cmd/vet
- go get github.com/golang/lint/golint
install:
- go get -v .
script:
- go test -v .
- go vet ./...
- go vet ./...
- golint ./...
48 changes: 48 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Sling Changelog

Notable changes between releases.

## v1.0.0 (2015-05-23)

* Added support for receiving and decoding error JSON structs
* Renamed Sling `JsonBody` setter to `BodyJSON` (breaking)
* Renamed Sling `BodyStruct` setter to `BodyForm` (breaking)
* Renamed Sling fields `httpClient`, `method`, `rawURL`, and `header` to be internal (breaking)
* Changed `Do` and `Receive` to skip response JSON decoding if "application/json" Content-Type is missing
* Changed `Sling.Receive(v interface{})` to `Sling.Receive(successV, failureV interface{})` (breaking)
* Previously `Receive` attempted to decode the response Body in all cases
* Updated `Receive` will decode the response Body into successV for 2XX responses or decode the Body into failureV for other status codes. Pass a nil `successV` or `failureV` to skip JSON decoding into that value.
* To upgrade, pass nil for the `failureV` argument or consider defining a JSON tagged struct appropriate for the API endpoint. (e.g. `s.Receive(&issue, nil)`, `s.Receive(&issue, &githubError)`)
* To retain the old behavior, duplicate the first argument (e.g. s.Receive(&tweet, &tweet))
* Changed `Sling.Do(http.Request, v interface{})` to `Sling.Do(http.Request, successV, failureV interface{})` (breaking)
* See the changelog entry about `Receive`, the upgrade path is the same.
* Removed HEAD, GET, POST, PUT, PATCH, DELETE constants, no reason to export them (breaking)

## v0.4.0 (2015-04-26)

* Improved golint compliance
* Fixed typos and test printouts

## v0.3.0 (2015-04-21)

* Added BodyStruct method for setting a url encoded form body on the Request
* Added Add and Set methods for adding or setting Request Headers
* Added JsonBody method for setting JSON Request Body
* Improved examples and documentation

## v0.2.0 (2015-04-05)

* Added http.Client setter
* Added Sling.New() method to return a copy of a Sling
* Added Base setter and Path extension support
* Added method setters (Get, Post, Put, Patch, Delete, Head)
* Added support for encoding URL Query parameters
* Added example tiny Github API
* Changed v0.1.0 method signatures and names (breaking)
* Removed Go 1.0 support

## v0.1.0 (2015-04-01)

* Support decoding JSON responses.


135 changes: 90 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@
# Sling [![Build Status](https://travis-ci.org/dghubble/sling.png)](https://travis-ci.org/dghubble/sling) [![Coverage](http://gocover.io/_badge/github.com/dghubble/sling)](http://gocover.io/github.com/dghubble/sling) [![GoDoc](http://godoc.org/github.com/dghubble/sling?status.png)](http://godoc.org/github.com/dghubble/sling)
<img align="right" src="https://s3.amazonaws.com/dghubble/small-gopher-with-sling.png">

Sling is a Go REST client library for creating and sending API requests.
Sling is a Go HTTP client library for creating and sending API requests.

Slings store http Request properties to simplify sending requests and decoding responses. Check [usage](#usage) or the [examples](examples) to learn how to compose a Sling into your API client.
Slings store HTTP Request properties to simplify sending requests and decoding responses. Check [usage](#usage) or the [examples](examples) to learn how to compose a Sling into your API client.

Note: Sling **v1.0** recently introduced some breaking changes. See [changes](CHANGES.md).

### Features

* Base/Path - path extend a Sling for different endpoints
* Method Setters: Get/Post/Put/Patch/Delete/Head
* Add and Set Request Headers
* Encode url structs into URL query parameters
* Encode url structs or json into the Request Body
* Decode received JSON success responses
* Encode structs into URL query parameters
* Encode a form or JSON into the Request Body
* Receive JSON success or failure responses

## Install

Expand All @@ -25,34 +27,36 @@ Read [GoDoc](https://godoc.org/github.com/dghubble/sling)

## Usage

Use a simple Sling to set request properties (`Path`, `QueryParams`, etc.) and then create a new `http.Request` by calling `Request()`.
Use a Sling to create an `http.Request` with a chained API for setting properties (path, method, queries, body, etc.).

```go
req, err := sling.New().Get("https://example.com").Request()
type Params struct {
Count int `url:"count,omitempty"`
}
params := &Params{Count: 5}

req, err := sling.New().Get("https://example.com").QueryStruct(params).Request()
client.Do(req)
```

Slings are much more powerful though. Use them to create REST clients which wrap complex API endpoints. Copy a base Sling with `New()` to avoid repeating common configuration.
### Path

```go
const twitterApi = "https://api.twitter.com/1.1/"
base := sling.New().Base(twitterApi).Client(httpAuthClient)
Use `Path` to set or extend the URL for created Requests. Extension means the path will be resolved relative to the existing URL.

users := base.New().Path("users/")
statuses := base.New().Path("statuses/")
```go
// sends a GET request to http://example.com/foo/bar
req, err := sling.New().Base("http://example.com/").Path("foo/").Path("bar").Request()
```

Choose an http method, set query parameters, and send the request.
Use `Get`, `Post`, `Put`, `Patch`, `Delete`, or `Head` which are exactly the same as `Path` except they set the HTTP method too.

```go
statuses.New().Get("show.json").QueryStruct(params).Receive(tweet)
req, err := sling.New().Post("http://upload.com/gophers")
```

The sections below provide more details about setting headers, query parameters, body data, and decoding a typed response after sending.

### Headers

`Add` or `Set` headers which should be applied to all Requests created by a Sling.
`Add` or `Set` headers which should be applied to the Requests created by a Sling.

```go
base := sling.New().Base(baseUrl).Set("User-Agent", "Gophergram API Client")
Expand Down Expand Up @@ -85,9 +89,9 @@ req, err := githubBase.New().Get(path).QueryStruct(params).Request()

### Body

#### JsonBody
#### Json Body

Make a Sling include JSON in the Body of its Requests using `JsonBody`.
Make a Sling include JSON in the Body of its Requests using `BodyJSON`.

```go
type IssueRequest struct {
Expand All @@ -107,14 +111,14 @@ body := &IssueRequest{
Title: "Test title",
Body: "Some issue",
}
req, err := githubBase.New().Post(path).JsonBody(body).Request()
req, err := githubBase.New().Post(path).BodyJSON(body).Request()
```

Requests will include an `application/json` Content-Type header.

#### BodyStruct
#### Form Body

Make a Sling include a url-tagged struct as a url-encoded form in the Body of its Requests using `BodyStruct`.
Make a Sling include a url-tagged struct as a url-encoded form in the Body of its Requests using `BodyForm`.

```go
type StatusUpdateParams struct {
Expand All @@ -126,33 +130,81 @@ type StatusUpdateParams struct {

```go
tweetParams := &StatusUpdateParams{Status: "writing some Go"}
req, err := twitterBase.New().Post(path).BodyStruct(tweetParams).Request()
req, err := twitterBase.New().Post(path).BodyForm(tweetParams).Request()
```

Requests will include an `application/x-www-form-urlencoded` Content-Type header.

### Extend a

Each distinct Sling generates an `http.Request` (say with some path and query
params) each time `Request()` is called, based on its state. When creating
different kinds of requests using distinct Slings, you may wish to extend
an existing Sling to minimize duplication (e.g. a common client).

Each Sling instance provides a `New()` method which creates an independent copy, so setting properties on the child won't mutate the parent Sling.

```go
const twitterApi = "https://api.twitter.com/1.1/"
base := sling.New().Base(twitterApi).Client(httpAuthClient)

// statuses/show.json Sling
tweetShowSling := base.New().Get("statuses/show.json").QueryStruct(params)
req, err := tweetShowSling.Request()

// statuses/update.json Sling
tweetPostSling := base.New().Post("statuses/update.json").BodyForm(params)
req, err := tweetPostSling.Request()
```

Without the calls to `base.New()`, tweetShowSling and tweetPostSling reference
the base Sling and POST to
"https://api.twitter.com/1.1/statuses/show.json/statuses/update.json", which
is undesired.

Recap: If you wish to extend a Sling, create a new child copy with `New()`.

### Receive

Define expected value structs. Use `Receive(v interface{})` to send a new Request that will automatically decode the response into the value.
Define a JSON struct to decode a type from 2XX success responses. Use `ReceiveSuccess(successV interface{})` to send a new Request and decode the response body into `successV` if it succeeds.

```go
// Github Issue (abbreviated)
type Issue struct {
Id int `json:"id"`
Url string `json:"url"`
Number int `json:"number"`
State string `json:"state"`
Title string `json:"title"`
Body string `json:"body"`
}
```

```go
issues := new([]Issue)
resp, err := githubBase.New().Get(path).QueryStruct(params).Receive(issues)
resp, err := githubBase.New().Get(path).QueryStruct(params).ReceiveSuccess(issues)
fmt.Println(issues, resp, err)
```

Most APIs return failure responses with JSON error details. To decode these, define success and failure JSON structs. Use `Receive(successV, failureV interface{})` to send a new Request that will automatically decode the response into the `successV` for 2XX responses or into `failureV` for non-2XX responses.

```go
type GithubError struct {
Message string `json:"message"`
Errors []struct {
Resource string `json:"resource"`
Field string `json:"field"`
Code string `json:"code"`
} `json:"errors"`
DocumentationURL string `json:"documentation_url"`
}
```

```go
issues := new([]Issue)
githubError := new(GithubError)
resp, err := githubBase.New().Get(path).QueryStruct(params).Receive(issues, githubError)
fmt.Println(issues, githubError, resp, err)
```

Pass a nil `successV` or `failureV` argument to skip JSON decoding into that value.

### Build an API

APIs typically define an endpoint (also called a service) for each type of resource. For example, here is a tiny Github IssueService which [creates](https://developer.github.com/v3/issues/#create-an-issue) and [lists](https://developer.github.com/v3/issues/#list-issues-for-a-repository) repository issues.
Expand All @@ -164,21 +216,18 @@ type IssueService struct {

func NewIssueService(httpClient *http.Client) *IssueService {
return &IssueService{
sling: sling.New().Client(httpClient).Base(baseUrl),
sling: sling.New().Client(httpClient).Base(baseURL),
}
}

func (s *IssueService) Create(owner, repo string, issueBody *IssueRequest) (*Issue, *http.Response, error) {
issue := new(Issue)
func (s *IssueService) ListByRepo(owner, repo string, params *IssueListParams) ([]Issue, *http.Response, error) {
issues := new([]Issue)
githubError := new(GithubError)
path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
resp, err := s.sling.New().Post(path).JsonBody(issueBody).Receive(issue)
return issue, resp, err
}

func (srvc IssueService) List(owner, repo string, params *IssueParams) ([]Issue, *http.Response, error) {
var issues []Issue
path := fmt.Sprintf("repos/%s/%s/issues", owner, repo)
resp, err := srvc.sling.New().Get(path).QueryStruct(params).Receive(&issues)
resp, err := s.sling.New().Get(path).QueryStruct(params).Receive(issues, githubError)
if err == nil {
err = githubError
}
return *issues, resp, err
}
```
Expand All @@ -189,10 +238,6 @@ func (srvc IssueService) List(owner, repo string, params *IssueParams) ([]Issue,

Create a Pull Request to add a link to your own API.

## Roadmap

* Receive custom error structs

## Motivation

Many client libraries follow the lead of [google/go-github](https://github.com/google/go-github) (our inspiration!), but do so by reimplementing logic common to all clients.
Expand Down
Loading