Skip to content

Commit

Permalink
Merge pull request #73 from danielharasymiw-wf/add-middleware-support
Browse files Browse the repository at this point in the history
Add Rest client middleware with unit tests
  • Loading branch information
tylertreat-wf authored Aug 2, 2016
2 parents edb1b3f + 0455547 commit 6d6bf2e
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 5 deletions.
92 changes: 87 additions & 5 deletions rest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
)

// Supported HTTP Methods
Expand All @@ -33,10 +34,52 @@ const (
httpPut = "PUT"
)

// InvocationHandler is a function that is to be wrapped by the ClientMiddleware
type InvocationHandler func(c *http.Client, method, url string, body interface{}, header http.Header) (*Response, error)

// ClientMiddleware is a function that wraps another function, and returns the wrapped function
type ClientMiddleware func(InvocationHandler) InvocationHandler

// HttpClient is the type that is used to perform HTTP Methods
type HttpClient interface {
Do(req *http.Request) (resp *http.Response, err error)
Get(url string) (resp *http.Response, err error)
Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error)
PostForm(url string, data url.Values) (resp *http.Response, err error)
Head(url string) (resp *http.Response, err error)
}

// RestClient performs HTTP methods including Get, Post, Put, and Delete
type RestClient interface {
// Get will perform an HTTP GET on the specified URL and return the response.
Get(url string, header http.Header) (*Response, error)

// Post will perform an HTTP POST on the specified URL and return the response.
Post(url string, body interface{}, header http.Header) (*Response, error)

// Put will perform an HTTP PUT on the specified URL and return the response.
Put(url string, body interface{}, header http.Header) (*Response, error)

// Delete will perform an HTTP DELETE on the specified URL and return the response.
Delete(url string, header http.Header) (*Response, error)
}

func NewRestClient(c HttpClient, middleware ...ClientMiddleware) RestClient {
return &client{c, middleware}
}

// Client is the type that encapsulates and uses the Authorizer to sign any REST
// requests that are performed.
type Client struct {
*http.Client
HttpClient
}

// client is the type that encapsulates and uses the Authorizer to sign any REST
// requests that are performed and has a list of middlewares to be applied to
// it's GET, POST, PUT, and DELETE functions.
type client struct {
HttpClient
middleware []ClientMiddleware
}

// Response is unmarshaled struct returned from an HTTP request.
Expand All @@ -62,24 +105,63 @@ func (rde *ResponseDecodeError) Error() string {
rde.DecodeError.Error(), rde.StatusCode, rde.Status, string(rde.Response))
}

// applyMiddleware wraps a given InvocationHandler with all of the middleware in the client
func (c *client) applyMiddleware(method InvocationHandler) InvocationHandler {
for _, middleware := range c.middleware {
method = middleware(method)
}
return method
}

func (c *client) process(method, url string, body interface{}, header http.Header) (*Response, error) {
m := c.applyMiddleware(do)
return m(c.HttpClient.(*http.Client), method, url, body, header)
}

// Get will perform an HTTP GET on the specified URL and return the response.
func (c *client) Get(url string, header http.Header) (*Response, error) {
return c.process(httpGet, url, nil, header)
}

// Post will perform an HTTP POST on the specified URL and return the response.
func (c *client) Post(url string, body interface{}, header http.Header) (*Response, error) {
return c.process(httpPost, url, body, header)
}

// Put will perform an HTTP PUT on the specified URL and return the response.
func (c *client) Put(url string, body interface{}, header http.Header) (*Response, error) {
return c.process(httpPut, url, body, header)
}

// Delete will perform an HTTP DELETE on the specified URL and return the response.
func (c *client) Delete(url string, header http.Header) (*Response, error) {
return c.process(httpDelete, url, nil, header)
}

/* Internal client calls. Would like to move up to the top level with a major
version change. Keeping this around for now to maintain backwards compat and allow
consumers to still construct the Client struct directly vs using the new constructor
method that returns the internal implementation that maps to the interface.
*/

// Get will perform an HTTP GET on the specified URL and return the response.
func (c *Client) Get(url string, header http.Header) (*Response, error) {
return do(c.Client, httpGet, url, nil, header)
return do(c.HttpClient.(*http.Client), httpGet, url, nil, header)
}

// Post will perform an HTTP POST on the specified URL and return the response.
func (c *Client) Post(url string, body interface{}, header http.Header) (*Response, error) {
return do(c.Client, httpPost, url, body, header)
return do(c.HttpClient.(*http.Client), httpPost, url, body, header)
}

// Put will perform an HTTP PUT on the specified URL and return the response.
func (c *Client) Put(url string, body interface{}, header http.Header) (*Response, error) {
return do(c.Client, httpPut, url, body, header)
return do(c.HttpClient.(*http.Client), httpPut, url, body, header)
}

// Delete will perform an HTTP DELETE on the specified URL and return the response.
func (c *Client) Delete(url string, header http.Header) (*Response, error) {
return do(c.Client, httpDelete, url, nil, header)
return do(c.HttpClient.(*http.Client), httpDelete, url, nil, header)
}

var do = func(c *http.Client, method, url string, body interface{}, header http.Header) (*Response, error) {
Expand Down
158 changes: 158 additions & 0 deletions rest/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,161 @@ func TestDoDecodeResult(t *testing.T) {

assert.Nil(err)
}

// Ensures that Get invokes do with the correct HTTP client, method, url and
// header, and that middlewares are applied.
func TestNewClientGet(t *testing.T) {
assert := assert.New(t)
middlewaresApplied := 0
middleware := func(next InvocationHandler) InvocationHandler {
return InvocationHandler(func(c *http.Client, method string, url string, body interface{}, header http.Header) (*Response, error) {
middlewaresApplied++
return next(c, method, url, body, header)
})
}
middlewares := []ClientMiddleware{middleware, middleware}

httpClient := http.DefaultClient
client := &client{httpClient, middlewares}
header := http.Header{}
url := "http://localhost"
mockResponse := &Response{}
mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) {
assert.Equal(httpClient, c)
assert.Equal(httpGet, m)
assert.Nil(b)
assert.Equal(header, h)
assert.Equal(url, u)
return mockResponse, nil
}

before := do
do = mockDo

resp, err := client.Get(url, header)

assert.Equal(mockResponse, resp)
assert.Equal(middlewaresApplied, 2)
assert.Nil(err)

do = before
}

// Ensures that Post invokes do with the correct HTTP client, method, url and
// header, and that middlewares are applied.
func TestNewClientPost(t *testing.T) {
assert := assert.New(t)
middlewaresApplied := 0
middleware := func(next InvocationHandler) InvocationHandler {
return InvocationHandler(func(c *http.Client, method string, url string, body interface{}, header http.Header) (*Response, error) {
middlewaresApplied++
return next(c, method, url, body, header)
})
}
middlewares := []ClientMiddleware{middleware, middleware}

httpClient := http.DefaultClient
client := &client{httpClient, middlewares}
header := http.Header{}
url := "http://localhost"
body := "foo"
mockResponse := &Response{}
mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) {
assert.Equal(httpClient, c)
assert.Equal(httpPost, m)
assert.Equal(body, b)
assert.Equal(header, h)
assert.Equal(url, u)
return mockResponse, nil
}

before := do
do = mockDo

resp, err := client.Post(url, body, header)

assert.Equal(mockResponse, resp)
assert.Equal(middlewaresApplied, 2)
assert.Nil(err)

do = before
}

// Ensures that Put invokes do with the correct HTTP client, method, url and
// header, and that middlewares are applied.
func TestNewClientPut(t *testing.T) {
assert := assert.New(t)
middlewaresApplied := 0
middleware := func(next InvocationHandler) InvocationHandler {
return InvocationHandler(func(c *http.Client, method string, url string, body interface{}, header http.Header) (*Response, error) {
middlewaresApplied++
return next(c, method, url, body, header)
})
}
middlewares := []ClientMiddleware{middleware, middleware}

httpClient := http.DefaultClient
client := &client{httpClient, middlewares}
header := http.Header{}
url := "http://localhost"
body := "foo"
mockResponse := &Response{}
mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) {
assert.Equal(httpClient, c)
assert.Equal(httpPut, m)
assert.Equal(body, b)
assert.Equal(header, h)
assert.Equal(url, u)
return mockResponse, nil
}

before := do
do = mockDo

resp, err := client.Put(url, body, header)

assert.Equal(mockResponse, resp)
assert.Equal(middlewaresApplied, 2)
assert.Nil(err)

do = before
}

// Ensures that Delete invokes do with the correct HTTP client, method, url and
// header, and that middlewares are applied.
func TestNewClientDelete(t *testing.T) {
assert := assert.New(t)
middlewaresApplied := 0
middleware := func(next InvocationHandler) InvocationHandler {
return InvocationHandler(func(c *http.Client, method string, url string, body interface{}, header http.Header) (*Response, error) {
middlewaresApplied++
return next(c, method, url, body, header)
})
}
middlewares := []ClientMiddleware{middleware, middleware}

httpClient := http.DefaultClient
client := &client{httpClient, middlewares}
header := http.Header{}
url := "http://localhost"
mockResponse := &Response{}
mockDo := func(c *http.Client, m, u string, b interface{}, h http.Header) (*Response, error) {
assert.Equal(httpClient, c)
assert.Equal(httpDelete, m)
assert.Nil(b)
assert.Equal(header, h)
assert.Equal(url, u)
return mockResponse, nil
}

before := do
do = mockDo

resp, err := client.Delete(url, header)

assert.Equal(mockResponse, resp)
assert.Equal(middlewaresApplied, 2)
assert.Nil(err)

do = before
}

0 comments on commit 6d6bf2e

Please sign in to comment.