Skip to content

Commit

Permalink
feat(client): Allow custom headers (#9)
Browse files Browse the repository at this point in the history
Support for better options for the `Client` including custom headers.
Docs & Changelog update


Co-authored-by: Alexej Kubarev <[email protected]>
  • Loading branch information
Nightapes and alexejk committed Jan 24, 2020
1 parent 7926ea3 commit 6ccfe9d
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 81 deletions.
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
## 0.1.3 (WIP)
## 0.2.x

## 0.2.0

Improvements:

* User-Agent can now be configured on the Client (#6)
* `NewClient` supports receiving a list of `Option`s that modify clients behavior.
Initial supported options are:

* `HttpClient(*http.Client)` - set custom `http.Client` to be used
* `Headers(map[string]string)` - set custom headers to use in every request (kudos: @Nightapes)
* `UserAgent(string)` - set User-Agent identification to be used (#6). This is a shortcut for just setting `User-Agent` custom header

Deprecations:

* `NewCustomClient` is deprecated in favor of `NewClient(string, ...Option)` with `HttpClient(*http.Client)` option.
This method will be removed in future versions.

## 0.1.2

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ func main() {
}
```

If you want to customize any aspect of `http.Client` used to perform requests, use `NewClientWithHttpClient` instead.
By default `http.DefaultClient` will be used.
Customization is supported by passing a list of `Option` to the `NewClient` function.
For instance:

- To customize any aspect of `http.Client` used to perform requests, use `HttpClient` option, otherwise `http.DefaultClient` will be used
- To pass custom headers, make use of `Headers` option.

### Argument encoding

Expand Down
29 changes: 12 additions & 17 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,33 @@ type Client struct {

// NewClient creates a Client with http.DefaultClient.
// If provided endpoint is not valid, an error is returned.
func NewClient(endpoint string) (*Client, error) {

return NewClientWithHttpClient(endpoint, http.DefaultClient)
}

// NewClientWithHttpClient allows customization of http.Client used to make RPC calls.
// If provided endpoint is not valid, an error is returned.
func NewClientWithHttpClient(endpoint string, httpClient *http.Client) (*Client, error) {
func NewClient(endpoint string, opts ...Option) (*Client, error) {

// Parse Endpoint URL
endpointUrl, err := url.Parse(endpoint)
if err != nil {
return nil, fmt.Errorf("invalid endpoint url: %w", err)
}

codec := NewCodec(endpointUrl, httpClient)
codec := NewCodec(endpointUrl, http.DefaultClient)

c := &Client{
codec: codec,
Client: rpc.NewClientWithCodec(codec),
}

// Apply options
for _, opt := range opts {
opt(c)
}

return c, nil
}

// UserAgent returns currently configured User-Agent header that will be sent to remote server on every RPC call.
func (c *Client) UserAgent() string {
return c.codec.userAgent
}
// NewCustomClient allows customization of http.Client used to make RPC calls.
// If provided endpoint is not valid, an error is returned.
// Deprecated: prefer using NewClient with HttpClient Option
func NewCustomClient(endpoint string, httpClient *http.Client) (*Client, error) {

// SetUserAgent allows customization to User-Agent header.
// If set to an empty string, User-Agent header will be sent with an empty value.
func (c *Client) SetUserAgent(ua string) {
c.codec.userAgent = ua
return NewClient(endpoint, HttpClient(httpClient))
}
65 changes: 11 additions & 54 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ import (
"github.com/stretchr/testify/assert"
)

func TestNewClient(t *testing.T) {
c, err := NewClient(":8080/rpc")

assert.Error(t, err)
assert.Nil(t, c)

c, err = NewClient("http://localhost")
assert.NoError(t, err)
assert.NotNil(t, c)
}

func TestClient_Call(t *testing.T) {

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -57,60 +68,6 @@ func TestClient_Call(t *testing.T) {
assert.Equal(t, 12345, resp.Index)
}

func TestClient_SetUserAgent(t *testing.T) {
tests := []struct {
name string
skipSet bool
agent string
expect string
}{
{
name: "default user-agent",
skipSet: true,
expect: defaultUserAgent,
},
{
name: "new user-agent",
agent: "my-new-agent/1.2.3",
expect: "my-new-agent/1.2.3",
},
{
name: "empty user-agent",
agent: "",
expect: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

serverCalled := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

ua := r.UserAgent()

assert.Equal(t, tt.expect, ua)

serverCalled = true
_, _ = fmt.Fprintln(w, string(loadTestFile(t, "response_simple.xml")))
}))
defer ts.Close()

c, err := NewClient(ts.URL)
assert.NoError(t, err)

if !tt.skipSet {
c.SetUserAgent(tt.agent)
}
assert.Equal(t, tt.expect, c.UserAgent())
err = c.Call("test.Method", nil, nil)
assert.NoError(t, err)

assert.True(t, serverCalled, "server must be called")
})
}
}

func TestClient_Fault(t *testing.T) {

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
18 changes: 12 additions & 6 deletions codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ const defaultUserAgent = "alexejk.io/go-xmlrpc"
// Codec implements methods required by rpc.ClientCodec
// In this implementation Codec is the one performing actual RPC requests with http.Client.
type Codec struct {
endpoint *url.URL
httpClient *http.Client
endpoint *url.URL
httpClient *http.Client
customHeaders map[string]string

mutex sync.Mutex
// contains completed but not processed responses by sequence ID
Expand Down Expand Up @@ -46,9 +47,8 @@ func NewCodec(endpoint *url.URL, httpClient *http.Client) *Codec {
return &Codec{
endpoint: endpoint,
httpClient: httpClient,

encoder: &StdEncoder{},
decoder: &StdDecoder{},
encoder: &StdEncoder{},
decoder: &StdDecoder{},

pending: make(map[uint64]*rpcCall),
response: nil,
Expand All @@ -72,9 +72,15 @@ func (c *Codec) WriteRequest(req *rpc.Request, args interface{}) error {
}

httpRequest.Header.Set("Content-Type", "text/xml")
httpRequest.Header.Set("Content-Length", fmt.Sprintf("%d", bodyBuffer.Len()))
httpRequest.Header.Set("User-Agent", c.userAgent)

// Apply customer headers if set, this allows overwriting static default headers
for key, value := range c.customHeaders {
httpRequest.Header.Set(key, value)
}

httpRequest.Header.Set("Content-Length", fmt.Sprintf("%d", bodyBuffer.Len()))

httpResponse, err := c.httpClient.Do(httpRequest)
if err != nil {
return err
Expand Down
5 changes: 5 additions & 0 deletions decode_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ func NewResponse(body []byte) (*Response, error) {
return response, nil
}

// ResponseParam encapsulates a nested parameter value
type ResponseParam struct {
Value ResponseValue `xml:"value"`
}

// ResponseValue encapsulates one of the data types for each parameter.
// Only one field should be set.
type ResponseValue struct {
Array []*ResponseValue `xml:"array>data>value"`
Struct []*ResponseStructMember `xml:"struct>member"`
Expand All @@ -37,11 +40,13 @@ type ResponseValue struct {
Base64 string `xml:"base64"`
}

// ResponseStructMember contains name-value pair of the struct
type ResponseStructMember struct {
Name string `xml:"name"`
Value ResponseValue `xml:"value"`
}

// ResponseFault wraps around failure
type ResponseFault struct {
Value ResponseValue `xml:"value"`
}
3 changes: 3 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ The simplest use-case is creating a client towards an endpoint and making calls:
err = c.Call("Bugzilla.version", nil, resp)
fmt.Printf("Version: %s\n", resp.BugzillaVersion.Version)
Additional customizations, such as setting custom headers, changing User-Agent or modifying HTTP Client used to make calls,
pass corresponding Options to NewClient function.
*/
package xmlrpc
28 changes: 28 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package xmlrpc

import "net/http"

// Option is a function that configures a Client by mutating it
type Option func(client *Client)

// Headers option allows setting custom headers that will be passed with every request
func Headers(headers map[string]string) Option {
return func(client *Client) {
client.codec.customHeaders = headers
}
}

// HttpClient option allows setting custom HTTP Client to be used for every request
func HttpClient(httpClient *http.Client) Option {
return func(client *Client) {
client.codec.httpClient = httpClient
}
}

// UserAgent option allows setting custom User-Agent header.
// This is a convenience method when only UA needs to be modified. For other cases use Headers option.
func UserAgent(userAgent string) Option {
return func(client *Client) {
client.codec.userAgent = userAgent
}
}
Loading

0 comments on commit 6ccfe9d

Please sign in to comment.