├── LICENSE ├── README.md ├── client.go ├── client_test.go ├── error.go ├── examples ├── create_gitub_repo │ └── main.go ├── handle_github_error │ └── main.go └── list_github_repos │ └── main.go ├── main_test.go ├── options.go ├── options_test.go ├── request_builder.go ├── request_builder_test.go ├── request_doer.go ├── response_reader.go └── response_reader_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017, Zack Patrick 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RClient 2 | 3 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/zpatrick/rclient/blob/master/LICENSE) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/zpatrick/rclient)](https://goreportcard.com/report/github.com/zpatrick/rclient) 5 | [![Go Doc](https://godoc.org/github.com/zpatrick/rclient?status.svg)](https://godoc.org/github.com/zpatrick/rclient) 6 | 7 | ## Getting Started 8 | Checkout the [Examples](https://github.com/zpatrick/rclient/tree/master/examples) folder for some working examples. 9 | The following snippet shows RClient interacting with Github's API: 10 | ``` 11 | package main 12 | 13 | import ( 14 | "github.com/zpatrick/rclient" 15 | "log" 16 | ) 17 | 18 | type Repository struct { 19 | Name string `json:"name"` 20 | } 21 | 22 | func main() { 23 | client := rclient.NewRestClient("https://api.github.com") 24 | 25 | var repos []Repository 26 | if err := client.Get("/users/zpatrick/repos", &repos); err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | log.Println(repos) 31 | } 32 | ``` 33 | 34 | ## Request Options 35 | Requests can be configured using a [Request Option](https://godoc.org/github.com/zpatrick/rclient#RequestOption). 36 | A `RequestOption` is simply a function that manipulates an `http.Request`. 37 | You can create request options like so: 38 | ``` 39 | setProto := func(req *http.Request) error { 40 | req.Proto = "HTTP/1.0" 41 | return nil 42 | } 43 | 44 | client.Get("/path", &v, setProto) 45 | ``` 46 | 47 | The built-in request options are described below. 48 | 49 | #### Header / Headers 50 | The `Header()` and `Headers()` options add header(s) to a `*http.Request`. 51 | ``` 52 | // add a single header 53 | client.Get("/path", &v, rclient.Header("name", "val")) 54 | 55 | // add multiple headers 56 | client.Get("/path", &v, rclient.Header("name1", "val1"), rclient.Header("name2", "val2")) 57 | client.Get("/path", &v, rclient.Headers(map[string]string{"name1": "val1", "name2":"val2"})) 58 | ``` 59 | 60 | #### Basic Auth 61 | The `BasicAuth()` option adds basic auth to a `*http.Request`. 62 | ``` 63 | client.Get("/path", &v, rclient.BasicAuth("user", "pass")) 64 | ``` 65 | 66 | #### Query 67 | The `Query()` options adds a query to a `*http.Request`. 68 | ``` 69 | query := url.Values{} 70 | query.Add("name", "John") 71 | query.Add("age", "35") 72 | 73 | client.Get("/path", &v, rclient.Query(query)) 74 | ``` 75 | 76 | **NOTE**: This can also be accomplished by adding the raw query to the `path` argument 77 | ``` 78 | client.Get("/path?name=John&age=35", &v) 79 | ``` 80 | 81 | ## Client Configuration 82 | The `RestClient` can be configured using the [Client Options](https://godoc.org/github.com/zpatrick/rclient#ClientOption) described below. 83 | 84 | #### Doer 85 | The `Doer()` option sets the `RequestDoer` field on the `RestClient`. 86 | This is the `http.DefaultClient` by default, and it can be set to anything that satisfies the [RequestDoer](https://godoc.org/github.com/zpatrick/rclient#RequestDoer) interface. 87 | ``` 88 | client, err := rclient.NewRestClient("https://api.github.com", rclient.Doer(&http.Client{})) 89 | ``` 90 | 91 | #### Request Options 92 | The `RequestOptions()` option sets the `RequestOptions` field on the `RestClient`. 93 | This will manipulate each request made by the `RestClient`. 94 | This can be any of the options described in the [Request Options](#request-options) section. 95 | A typical use-case would be adding headers for each request. 96 | ``` 97 | options := []rclient.RequestOption{ 98 | rclient.Header("name", "John Doe"). 99 | rclient.Header("token", "abc123"), 100 | } 101 | 102 | client, err := rclient.NewRestClient("https://api.github.com", rclient.RequestOptions(options...)) 103 | ``` 104 | 105 | #### Builder 106 | The `Builder()` option sets the `RequestBuilder` field on the `RestClient`. 107 | This field is responsible for building `*http.Request` objects. 108 | This is the [BuildJSONRequest](https://godoc.org/github.com/zpatrick/rclient#BuildJSONRequest) function by default, and it can be set to any [RequestBuilder](https://godoc.org/github.com/zpatrick/rclient#RequestBuilder) function. 109 | ``` 110 | builder := func(method, url string, body interface{}, options ...RequestOption) (*http.Request, error){ 111 | req, _ := http.NewRequest(method, url, nil) 112 | for _, option := range options { 113 | if err := option(req); err != nil { 114 | return nil, err 115 | } 116 | } 117 | 118 | return nil, errors.New("I forgot to add a body to the request!") 119 | } 120 | 121 | client, err := rclient.NewRestClient("https://api.github.com", rclient.Builder(builder)) 122 | ``` 123 | 124 | #### Reader 125 | The `Reader()` option sets the `ResponseReader` field on the `RestClient`. 126 | This field is responsible for reading `*http.Response` objects. 127 | This is the [ReadJSONResponse](https://godoc.org/github.com/zpatrick/rclient#ReadJSONResponse) function by default, and it can be set to any [ResponseReader](https://godoc.org/github.com/zpatrick/rclient#ResponseReader) function. 128 | ``` 129 | reader := func(resp *http.Response, v interface{}) error{ 130 | defer resp.Body.Close() 131 | return json.NewDecoder(resp.Body).Decode(v) 132 | } 133 | 134 | client, err := rclient.NewRestClient("https://api.github.com", rclient.Reader(reader)) 135 | ``` 136 | 137 | # License 138 | This work is published under the MIT license. 139 | 140 | Please see the `LICENSE` file for details. 141 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // RestClient builds, executes, and reads http requests/responses. 9 | type RestClient struct { 10 | Host string 11 | RequestBuilder RequestBuilder 12 | RequestDoer RequestDoer 13 | ResponseReader ResponseReader 14 | RequestOptions []RequestOption 15 | } 16 | 17 | // NewRestClient returns a new RestClient with all of the default fields. 18 | // Any of the default fields can be changed with the options param. 19 | func NewRestClient(host string, options ...ClientOption) *RestClient { 20 | r := &RestClient{ 21 | Host: host, 22 | RequestBuilder: BuildJSONRequest, 23 | RequestDoer: http.DefaultClient, 24 | ResponseReader: ReadJSONResponse, 25 | RequestOptions: []RequestOption{}, 26 | } 27 | 28 | for _, option := range options { 29 | option(r) 30 | } 31 | 32 | return r 33 | } 34 | 35 | // Delete passes its params to RestClient.Do() with the "DELETE" method. 36 | func (r *RestClient) Delete(path string, body, v interface{}, options ...RequestOption) error { 37 | return r.Do("DELETE", path, body, v, options...) 38 | } 39 | 40 | // Get passes its params to RestClient.Do() with the "GET" method. 41 | func (r *RestClient) Get(path string, v interface{}, options ...RequestOption) error { 42 | return r.Do("GET", path, nil, v, options...) 43 | } 44 | 45 | // Patch passes its params to RestClient.Do() with the "PATCH" method. 46 | func (r *RestClient) Patch(path string, body, v interface{}, options ...RequestOption) error { 47 | return r.Do("PATCH", path, body, v, options...) 48 | } 49 | 50 | // Post passes its params to RestClient.Do() with the "POST" method. 51 | func (r *RestClient) Post(path string, body, v interface{}, options ...RequestOption) error { 52 | return r.Do("POST", path, body, v, options...) 53 | } 54 | 55 | // Put passes its params to RestClient.Do() with the "PUT" method. 56 | func (r *RestClient) Put(path string, body, v interface{}, options ...RequestOption) error { 57 | return r.Do("PUT", path, body, v, options...) 58 | } 59 | 60 | // Do orchestrates building, performing, and reading http requests and responses. 61 | func (r *RestClient) Do(method, path string, body, v interface{}, options ...RequestOption) error { 62 | url := fmt.Sprintf("%s%s", r.Host, path) 63 | options = append(r.RequestOptions, options...) 64 | 65 | req, err := r.RequestBuilder(method, url, body, options...) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | resp, err := r.RequestDoer.Do(req) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | return r.ResponseReader(resp, v) 76 | } 77 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var JohnDoe = person{Name: "John Doe", Age: 35} 12 | 13 | func TestClientDelete(t *testing.T) { 14 | handler := func(w http.ResponseWriter, r *http.Request) { 15 | assert.Equal(t, "DELETE", r.Method) 16 | assert.Equal(t, "/people/john", r.URL.Path) 17 | 18 | write(t, w, 200, nil) 19 | } 20 | 21 | client, server := newClientAndServer(t, handler) 22 | defer server.Close() 23 | 24 | if err := client.Delete("/people/john", nil, nil); err != nil { 25 | t.Error(err) 26 | } 27 | } 28 | 29 | func TestClientDeleteWithBody(t *testing.T) { 30 | handler := func(w http.ResponseWriter, r *http.Request) { 31 | assert.Equal(t, "DELETE", r.Method) 32 | assert.Equal(t, "/people", r.URL.Path) 33 | assert.Equal(t, JohnDoe, readPerson(t, r)) 34 | 35 | write(t, w, 200, JohnDoe) 36 | } 37 | 38 | client, server := newClientAndServer(t, handler) 39 | defer server.Close() 40 | 41 | var p person 42 | if err := client.Delete("/people", JohnDoe, &p); err != nil { 43 | t.Error(err) 44 | } 45 | 46 | assert.Equal(t, JohnDoe, p) 47 | } 48 | 49 | func TestClientGet(t *testing.T) { 50 | handler := func(w http.ResponseWriter, r *http.Request) { 51 | assert.Equal(t, "GET", r.Method) 52 | assert.Equal(t, "/people/john", r.URL.Path) 53 | 54 | write(t, w, 200, JohnDoe) 55 | } 56 | 57 | client, server := newClientAndServer(t, handler) 58 | defer server.Close() 59 | 60 | var p person 61 | if err := client.Get("/people/john", &p); err != nil { 62 | t.Error(err) 63 | } 64 | 65 | assert.Equal(t, JohnDoe, p) 66 | } 67 | 68 | func TestClientPatch(t *testing.T) { 69 | handler := func(w http.ResponseWriter, r *http.Request) { 70 | assert.Equal(t, "PATCH", r.Method) 71 | assert.Equal(t, "/people/john", r.URL.Path) 72 | assert.Equal(t, JohnDoe, readPerson(t, r)) 73 | 74 | write(t, w, 200, JohnDoe) 75 | } 76 | 77 | client, server := newClientAndServer(t, handler) 78 | defer server.Close() 79 | 80 | var p person 81 | if err := client.Patch("/people/john", JohnDoe, &p); err != nil { 82 | t.Error(err) 83 | } 84 | 85 | assert.Equal(t, JohnDoe, p) 86 | } 87 | 88 | func TestClientPost(t *testing.T) { 89 | handler := func(w http.ResponseWriter, r *http.Request) { 90 | assert.Equal(t, "POST", r.Method) 91 | assert.Equal(t, "/people", r.URL.Path) 92 | assert.Equal(t, JohnDoe, readPerson(t, r)) 93 | 94 | write(t, w, 201, JohnDoe) 95 | } 96 | 97 | client, server := newClientAndServer(t, handler) 98 | defer server.Close() 99 | 100 | var p person 101 | if err := client.Post("/people", JohnDoe, &p); err != nil { 102 | t.Error(err) 103 | } 104 | 105 | assert.Equal(t, JohnDoe, p) 106 | } 107 | 108 | func TestClientPut(t *testing.T) { 109 | handler := func(w http.ResponseWriter, r *http.Request) { 110 | assert.Equal(t, "PUT", r.Method) 111 | assert.Equal(t, "/people/john", r.URL.Path) 112 | assert.Equal(t, JohnDoe, readPerson(t, r)) 113 | 114 | write(t, w, 200, JohnDoe) 115 | } 116 | 117 | client, server := newClientAndServer(t, handler) 118 | defer server.Close() 119 | 120 | var p person 121 | if err := client.Put("/people/john", JohnDoe, &p); err != nil { 122 | t.Error(err) 123 | } 124 | 125 | assert.Equal(t, JohnDoe, p) 126 | } 127 | 128 | func TestClientDo(t *testing.T) { 129 | builder := func(method, url string, body interface{}, options ...RequestOption) (*http.Request, error) { 130 | assert.Equal(t, "POST", method) 131 | assert.Equal(t, "https://domain.com/path", url) 132 | assert.Equal(t, "body", body) 133 | assert.Len(t, options, 0) 134 | 135 | return nil, nil 136 | } 137 | 138 | doer := RequestDoerFunc(func(*http.Request) (*http.Response, error) { 139 | return nil, nil 140 | }) 141 | 142 | var p person 143 | reader := func(resp *http.Response, v interface{}) error { 144 | assert.Equal(t, p, v) 145 | return nil 146 | } 147 | 148 | client := NewRestClient("https://domain.com", Builder(builder), Doer(doer), Reader(reader)) 149 | if err := client.Post("/path", "body", p); err != nil { 150 | t.Fatal(err) 151 | } 152 | } 153 | 154 | func TestClientBuilderError(t *testing.T) { 155 | builder := func(string, string, interface{}, ...RequestOption) (*http.Request, error) { 156 | return nil, errors.New("some error") 157 | } 158 | 159 | client := NewRestClient("", Builder(builder)) 160 | if err := client.Get("/path", nil); err == nil { 161 | t.Fatal("Error was nil!") 162 | } 163 | } 164 | 165 | func TestClientDoerError(t *testing.T) { 166 | builder := func(string, string, interface{}, ...RequestOption) (*http.Request, error) { 167 | return nil, nil 168 | } 169 | 170 | doer := RequestDoerFunc(func(*http.Request) (*http.Response, error) { 171 | return nil, errors.New("some error") 172 | }) 173 | 174 | client := NewRestClient("", Builder(builder), Doer(doer)) 175 | if err := client.Get("/path", nil); err == nil { 176 | t.Fatal("Error was nil!") 177 | } 178 | } 179 | 180 | func TestClientReaderError(t *testing.T) { 181 | builder := func(string, string, interface{}, ...RequestOption) (*http.Request, error) { 182 | return nil, nil 183 | } 184 | 185 | doer := RequestDoerFunc(func(*http.Request) (*http.Response, error) { 186 | return nil, nil 187 | }) 188 | 189 | reader := func(*http.Response, interface{}) error { 190 | return errors.New("some error") 191 | } 192 | 193 | client := NewRestClient("", Builder(builder), Doer(doer), Reader(reader)) 194 | if err := client.Get("/path", nil); err == nil { 195 | t.Fatal("Error was nil!") 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type ResponseError struct { 9 | Response *http.Response 10 | Message string 11 | } 12 | 13 | func (e *ResponseError) Error() string { 14 | return e.Message 15 | } 16 | 17 | func NewResponseError(resp *http.Response, message string) *ResponseError { 18 | return &ResponseError{ 19 | Response: resp, 20 | Message: message, 21 | } 22 | } 23 | 24 | func NewResponseErrorf(resp *http.Response, format string, tokens ...interface{}) *ResponseError { 25 | return NewResponseError(resp, fmt.Sprintf(format, tokens...)) 26 | } 27 | -------------------------------------------------------------------------------- /examples/create_gitub_repo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/zpatrick/rclient" 9 | ) 10 | 11 | type repository struct { 12 | Name string `json:"name,omitempty"` 13 | } 14 | 15 | func main() { 16 | username := flag.String("u", "", "username for your github account") 17 | password := flag.String("p", "", "password for your github account") 18 | flag.Parse() 19 | 20 | if *username == "" || *password == "" { 21 | log.Fatal("username and password are required") 22 | } 23 | 24 | client := rclient.NewRestClient("https://api.github.com") 25 | 26 | var repo repository 27 | request := repository{Name: "my_sample_repo"} 28 | 29 | // add auth to the request 30 | if err := client.Post("/user/repos", request, &repo, rclient.BasicAuth(*username, *password)); err != nil { 31 | log.Fatalf("Failed to create repository: %v", err) 32 | } 33 | 34 | fmt.Printf("Successfully created repository %s\n", repo.Name) 35 | fmt.Printf("Press the Enter Key to delete this repository: ") 36 | fmt.Scanln() 37 | 38 | // also, you can set basic auth for each request the client makes 39 | client = rclient.NewRestClient("https://api.github.com", rclient.RequestOptions(rclient.BasicAuth(*username, *password))) 40 | 41 | path := fmt.Sprintf("/repos/%s/%s", *username, repo.Name) 42 | if err := client.Delete(path, nil, nil); err != nil { 43 | log.Fatalf("Failed to delete repository: %v", err) 44 | } 45 | 46 | fmt.Printf("Successfully deleted repository %s\n", repo.Name) 47 | } 48 | -------------------------------------------------------------------------------- /examples/handle_github_error/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/zpatrick/rclient" 10 | ) 11 | 12 | type repository struct { 13 | Name string `json:"name,omitempty"` 14 | } 15 | 16 | type githubError struct { 17 | Message string `json:"message"` 18 | DocumentationURL string `json:"documentation_url"` 19 | } 20 | 21 | func (g *githubError) Error() string { 22 | return g.Message 23 | } 24 | 25 | func githubResponseReader(resp *http.Response, v interface{}) error { 26 | defer resp.Body.Close() 27 | 28 | switch resp.StatusCode { 29 | case 400, 404: 30 | var ge *githubError 31 | if err := json.NewDecoder(resp.Body).Decode(&ge); err != nil { 32 | return err 33 | } 34 | 35 | return ge 36 | default: 37 | return json.NewDecoder(resp.Body).Decode(v) 38 | } 39 | } 40 | 41 | func main() { 42 | client := rclient.NewRestClient("https://api.github.com", rclient.Reader(githubResponseReader)) 43 | 44 | var repo repository 45 | if err := client.Get("/repos/zpatrick/invalid_repo_name", &repo); err != nil { 46 | text := fmt.Sprintf("Failed to get repo: %s\n", err.Error()) 47 | if err, ok := err.(*githubError); ok { 48 | text += fmt.Sprintf("Checkout Github API docs at: %s\n", err.DocumentationURL) 49 | } 50 | 51 | log.Fatal(text) 52 | } 53 | 54 | log.Println(repo) 55 | } 56 | -------------------------------------------------------------------------------- /examples/list_github_repos/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/zpatrick/rclient" 9 | ) 10 | 11 | type repository struct { 12 | Name string `json:"name"` 13 | } 14 | 15 | func main() { 16 | username := flag.String("u", "zpatrick", "username for your github account") 17 | flag.Parse() 18 | 19 | client := rclient.NewRestClient("https://api.github.com") 20 | 21 | var repos []repository 22 | path := fmt.Sprintf("/users/%s/repos", *username) 23 | if err := client.Get(path, &repos); err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | fmt.Printf("Repos for %s: \n", *username) 28 | for _, r := range repos { 29 | fmt.Println(r.Name) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | ) 11 | 12 | type person struct { 13 | Name string 14 | Age int 15 | } 16 | 17 | func readPerson(t *testing.T, r *http.Request) person { 18 | var p person 19 | read(t, r, &p) 20 | return p 21 | } 22 | 23 | func newClientAndServer(t *testing.T, handler http.HandlerFunc, options ...ClientOption) (*RestClient, *httptest.Server) { 24 | server := httptest.NewServer(handler) 25 | client := NewRestClient(server.URL, options...) 26 | 27 | return client, server 28 | } 29 | 30 | func write(t *testing.T, w http.ResponseWriter, status int, body interface{}) { 31 | b, err := json.Marshal(body) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | w.WriteHeader(status) 37 | fmt.Fprintln(w, string(b)) 38 | } 39 | 40 | func read(t *testing.T, r *http.Request, v interface{}) { 41 | b, err := ioutil.ReadAll(r.Body) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | if err := json.Unmarshal(b, v); err != nil { 47 | t.Fatal(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | ) 7 | 8 | // A ClientOption configures a *RestClient. 9 | type ClientOption func(client *RestClient) 10 | 11 | // Builder sets the RequestBuilder field of a RestClient. 12 | func Builder(builder RequestBuilder) ClientOption { 13 | return func(r *RestClient) { 14 | r.RequestBuilder = builder 15 | } 16 | } 17 | 18 | // Doer sets the RequestDoer field of a RestClient. 19 | func Doer(doer RequestDoer) ClientOption { 20 | return func(r *RestClient) { 21 | r.RequestDoer = doer 22 | } 23 | } 24 | 25 | // Reader sets the ResponseReader field of a RestClient. 26 | func Reader(reader ResponseReader) ClientOption { 27 | return func(r *RestClient) { 28 | r.ResponseReader = reader 29 | } 30 | } 31 | 32 | // RequestOptions sets the RequestOptions field of a RestClient. 33 | func RequestOptions(options ...RequestOption) ClientOption { 34 | return func(r *RestClient) { 35 | r.RequestOptions = append(r.RequestOptions, options...) 36 | } 37 | } 38 | 39 | // A RequestOption configures a *http.Request. 40 | type RequestOption func(req *http.Request) error 41 | 42 | // BasicAuth adds the specified username and password as basic auth to a request. 43 | func BasicAuth(user, pass string) RequestOption { 44 | return func(req *http.Request) error { 45 | req.SetBasicAuth(user, pass) 46 | return nil 47 | } 48 | } 49 | 50 | // Header adds the specified name and value as a header to a request. 51 | func Header(name, val string) RequestOption { 52 | return func(req *http.Request) error { 53 | req.Header.Add(name, val) 54 | return nil 55 | } 56 | } 57 | 58 | // Headers adds the specified names and values as headers to a request 59 | func Headers(headers map[string]string) RequestOption { 60 | return func(req *http.Request) error { 61 | for name, val := range headers { 62 | req.Header.Add(name, val) 63 | } 64 | 65 | return nil 66 | } 67 | } 68 | 69 | // Query adds the specified query to a request. 70 | func Query(query url.Values) RequestOption { 71 | return func(req *http.Request) error { 72 | req.URL.RawQuery = query.Encode() 73 | return nil 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBasicAuth(t *testing.T) { 12 | req, err := http.NewRequest("", "", nil) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | if err := BasicAuth("user", "pass")(req); err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | // user:pass base64 encoded 22 | assert.Equal(t, "Basic dXNlcjpwYXNz", req.Header.Get("Authorization")) 23 | } 24 | 25 | func TestHeader(t *testing.T) { 26 | req, err := http.NewRequest("", "", nil) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | if err := Header("name", "val")(req); err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | assert.Equal(t, "val", req.Header.Get("name")) 36 | } 37 | 38 | func TestHeaders(t *testing.T) { 39 | req, err := http.NewRequest("", "", nil) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | h := map[string]string{"name1": "v1", "name2": "v2"} 45 | if err := Headers(h)(req); err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | assert.Equal(t, "v1", req.Header.Get("name1")) 50 | assert.Equal(t, "v2", req.Header.Get("name2")) 51 | } 52 | 53 | func TestQuery(t *testing.T) { 54 | req, err := http.NewRequest("", "", nil) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | q := url.Values{} 60 | q.Set("k1", "v1") 61 | q.Set("k2", "v2") 62 | 63 | if err := Query(q)(req); err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | assert.Equal(t, "k1=v1&k2=v2", req.URL.RawQuery) 68 | } 69 | -------------------------------------------------------------------------------- /request_builder.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | // A RequestBuilder creates a *http.Request from the given parameters. 10 | // It is important that each option gets added to the generated request: 11 | // req, _ := http.NewRequest(...) 12 | // for _, option := range options 13 | // if err := option(req); err != nil { 14 | // return nil, err 15 | // } 16 | // } 17 | type RequestBuilder func(method, url string, body interface{}, options ...RequestOption) (*http.Request, error) 18 | 19 | // BuildJSONRequest creates a new *http.Request with the specified method, url and body in JSON format. 20 | func BuildJSONRequest(method, url string, body interface{}, options ...RequestOption) (*http.Request, error) { 21 | b := new(bytes.Buffer) 22 | if body != nil { 23 | if err := json.NewEncoder(b).Encode(body); err != nil { 24 | return nil, err 25 | } 26 | } 27 | 28 | req, err := http.NewRequest(method, url, b) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | if b.Len() > 0 { 34 | req.Header.Add("content-type", "application/json") 35 | } 36 | 37 | for _, option := range options { 38 | if err := option(req); err != nil { 39 | return nil, err 40 | } 41 | } 42 | 43 | return req, nil 44 | } 45 | -------------------------------------------------------------------------------- /request_builder_test.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestBuildJSONRequest(t *testing.T) { 13 | req, err := BuildJSONRequest("GET", "www.domain.com/path", "body", Header("name", "val")) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | assert.Equal(t, "GET", req.Method) 19 | assert.Equal(t, "www.domain.com/path", req.URL.String()) 20 | assert.Equal(t, "application/json", req.Header.Get("content-type")) 21 | assert.Equal(t, "val", req.Header.Get("name")) 22 | 23 | body, err := ioutil.ReadAll(req.Body) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | assert.Equal(t, "\"body\"\n", string(body)) 29 | } 30 | 31 | func TestBuildJSONRequestNoBody(t *testing.T) { 32 | req, err := BuildJSONRequest("GET", "www.domain.com/path", nil) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | assert.Equal(t, "", req.Header.Get("content-type")) 38 | } 39 | 40 | func TestBuildJSONRequest_optionError(t *testing.T) { 41 | option := func(req *http.Request) error { 42 | return errors.New("some error") 43 | } 44 | 45 | if _, err := BuildJSONRequest("GET", "www.domain.com/path", "body", option); err == nil { 46 | t.Fatal("Error was nil!") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /request_doer.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // A RequestDoer sends a *http.Request and returns a *http.Response. 8 | type RequestDoer interface { 9 | Do(*http.Request) (*http.Response, error) 10 | } 11 | 12 | // A RequestDoerFunc is a function that implements the RequestDoer interface. 13 | type RequestDoerFunc func(*http.Request) (*http.Response, error) 14 | 15 | // Do executes the RequestDoerFunc. 16 | func (d RequestDoerFunc) Do(req *http.Request) (*http.Response, error) { 17 | return d(req) 18 | } 19 | -------------------------------------------------------------------------------- /response_reader.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | // A ResponseReader attempts to read a *http.Response into v. 9 | type ResponseReader func(resp *http.Response, v interface{}) error 10 | 11 | // ReadJSONResponse attempts to marshal the response body into v 12 | // if and only if the response StatusCode is in the 200 range. 13 | // Otherwise, an error is thrown. 14 | // It assumes the response body is in JSON format. 15 | func ReadJSONResponse(resp *http.Response, v interface{}) error { 16 | defer resp.Body.Close() 17 | 18 | switch { 19 | case resp.StatusCode < 200, resp.StatusCode > 299: 20 | return NewResponseErrorf(resp, "Invalid status code: %d", resp.StatusCode) 21 | case v == nil: 22 | return nil 23 | default: 24 | if err := json.NewDecoder(resp.Body).Decode(v); err != nil { 25 | return NewResponseError(resp, err.Error()) 26 | } 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /response_reader_test.go: -------------------------------------------------------------------------------- 1 | package rclient 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestReadJSONResponse(t *testing.T) { 13 | resp := &http.Response{ 14 | StatusCode: 200, 15 | Body: ioutil.NopCloser(bytes.NewBufferString("\"body\"")), 16 | } 17 | 18 | var v string 19 | if err := ReadJSONResponse(resp, &v); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | assert.Equal(t, "body", v) 24 | } 25 | 26 | func TestReadJSONResponseNilV(t *testing.T) { 27 | resp := &http.Response{ 28 | StatusCode: 200, 29 | Body: ioutil.NopCloser(bytes.NewBufferString("\"body\"")), 30 | } 31 | 32 | if err := ReadJSONResponse(resp, nil); err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | 37 | func TestReadJSONResponseError_statusCode(t *testing.T) { 38 | codes := []int{0, 199, 300, 399, 400, 499, 500, 599} 39 | 40 | for _, c := range codes { 41 | resp := &http.Response{ 42 | StatusCode: c, 43 | Body: ioutil.NopCloser(bytes.NewBufferString("")), 44 | } 45 | 46 | if err := ReadJSONResponse(resp, nil); err == nil { 47 | t.Fatalf("%d: Error was nil!", c) 48 | } 49 | } 50 | } 51 | 52 | func TestReadJSONResponseError_invalidJSON(t *testing.T) { 53 | resp := &http.Response{ 54 | StatusCode: 200, 55 | Body: ioutil.NopCloser(bytes.NewBufferString("some_invalid_json")), 56 | } 57 | 58 | var p person 59 | if err := ReadJSONResponse(resp, &p); err == nil { 60 | t.Fatalf("Error was nil!") 61 | } 62 | } 63 | --------------------------------------------------------------------------------