Skip to content

Commit

Permalink
Introduce response decoder interface
Browse files Browse the repository at this point in the history
  • Loading branch information
maxbeutel committed Nov 20, 2018
1 parent e2624bf commit 5ce5b3a
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 26 deletions.
21 changes: 21 additions & 0 deletions response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package sling

import (
"encoding/json"
"net/http"
)

// ResponseDecoder decodes http responses into struct values.
type ResponseDecoder interface {
Decode(resp *http.Response, v interface{}) error
}

// jsonDecoder decodes http response JSON into a JSON-tagged struct value.
type jsonDecoder struct {
}

// Decode the Response Body into the value pointed to by v.
// Caller must provide a non-nil v and close the resp.Body.
func (d jsonDecoder) Decode(resp *http.Response, v interface{}) error {
return json.NewDecoder(resp.Body).Decode(v)
}
49 changes: 27 additions & 22 deletions sling.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package sling

import (
"encoding/base64"
"encoding/json"
"io"
"net/http"
"net/url"
Expand Down Expand Up @@ -37,15 +36,18 @@ type Sling struct {
queryStructs []interface{}
// body provider
bodyProvider BodyProvider
// response decoder
responseDecoder ResponseDecoder
}

// New returns a new Sling with an http DefaultClient.
func New() *Sling {
return &Sling{
httpClient: http.DefaultClient,
method: "GET",
header: make(http.Header),
queryStructs: make([]interface{}, 0),
httpClient: http.DefaultClient,
method: "GET",
header: make(http.Header),
queryStructs: make([]interface{}, 0),
responseDecoder: jsonDecoder{},
}
}

Expand All @@ -68,12 +70,13 @@ func (s *Sling) New() *Sling {
headerCopy[k] = v
}
return &Sling{
httpClient: s.httpClient,
method: s.method,
rawURL: s.rawURL,
header: headerCopy,
queryStructs: append([]interface{}{}, s.queryStructs...),
bodyProvider: s.bodyProvider,
httpClient: s.httpClient,
method: s.method,
rawURL: s.rawURL,
header: headerCopy,
queryStructs: append([]interface{}{}, s.queryStructs...),
bodyProvider: s.bodyProvider,
responseDecoder: s.responseDecoder,
}
}

Expand Down Expand Up @@ -336,6 +339,15 @@ func addHeaders(req *http.Request, header http.Header) {

// Sending

// ResponseDecoder sets the Sling's response decoder.
func (s *Sling) ResponseDecoder(decoder ResponseDecoder) *Sling {
if decoder == nil {
return s
}
s.responseDecoder = decoder
return s
}

// ReceiveSuccess creates a new HTTP request and returns the response. Success
// responses (2XX) are JSON decoded into the value pointed to by successV.
// Any error creating the request, sending it, or decoding a 2XX response
Expand Down Expand Up @@ -377,7 +389,7 @@ func (s *Sling) Do(req *http.Request, successV, failureV interface{}) (*http.Res

// Decode from json
if successV != nil || failureV != nil {
err = decodeResponseJSON(resp, successV, failureV)
err = decodeResponse(resp, s.responseDecoder, successV, failureV)
}
return resp, err
}
Expand All @@ -387,22 +399,15 @@ func (s *Sling) Do(req *http.Request, successV, failureV interface{}) (*http.Res
// otherwise. If the successV or failureV argument to decode into is nil,
// decoding is skipped.
// Caller is responsible for closing the resp.Body.
func decodeResponseJSON(resp *http.Response, successV, failureV interface{}) error {
func decodeResponse(resp *http.Response, decoder ResponseDecoder, successV, failureV interface{}) error {
if code := resp.StatusCode; 200 <= code && code <= 299 {
if successV != nil {
return decodeResponseBodyJSON(resp, successV)
return decoder.Decode(resp, successV)
}
} else {
if failureV != nil {
return decodeResponseBodyJSON(resp, failureV)
return decoder.Decode(resp, failureV)
}
}
return nil
}

// decodeResponseBodyJSON JSON decodes a Response Body into the value pointed
// to by v.
// Caller must provide a non-nil v and close the resp.Body.
func decodeResponseBodyJSON(resp *http.Response, v interface{}) error {
return json.NewDecoder(resp.Body).Decode(v)
}
52 changes: 48 additions & 4 deletions sling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sling

import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"io"
Expand All @@ -28,15 +29,22 @@ var paramsA = struct {
}
var paramsB = FakeParams{KindName: "recent", Count: 25}

// Json-tagged model struct
// Json/XML-tagged model struct
type FakeModel struct {
Text string `json:"text,omitempty"`
FavoriteCount int64 `json:"favorite_count,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
Text string `json:"text,omitempty" xml:"text"`
FavoriteCount int64 `json:"favorite_count,omitempty" xml:"favorite_count"`
Temperature float64 `json:"temperature,omitempty" xml:"temperature"`
}

var modelA = FakeModel{Text: "note", FavoriteCount: 12}

// Non-Json response decoder
type xmlResponseDecoder struct{}

func (d xmlResponseDecoder) Decode(resp *http.Response, v interface{}) error {
return xml.NewDecoder(resp.Body).Decode(v)
}

func TestNew(t *testing.T) {
sling := New()
if sling.httpClient != http.DefaultClient {
Expand Down Expand Up @@ -717,6 +725,42 @@ func TestDo_onFailureWithNilValue(t *testing.T) {
}
}

func TestReceive_success_nonDefaultDecoder(t *testing.T) {
client, mux, server := testServer()
defer server.Close()
mux.HandleFunc("/foo/submit", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
data := ` <response>
<text>Some text</text>
<favorite_count>24</favorite_count>
<temperature>10.5</temperature>
</response>`
fmt.Fprintf(w, xml.Header)
fmt.Fprintf(w, data)
})

endpoint := New().Client(client).Base("http://example.com/").Path("foo/").Post("submit")

model := new(FakeModel)
apiError := new(APIError)
resp, err := endpoint.New().ResponseDecoder(xmlResponseDecoder{}).Receive(model, apiError)

if err != nil {
t.Errorf("expected nil, got %v", err)
}
if resp.StatusCode != 200 {
t.Errorf("expected %d, got %d", 200, resp.StatusCode)
}
expectedModel := &FakeModel{Text: "Some text", FavoriteCount: 24, Temperature: 10.5}
if !reflect.DeepEqual(expectedModel, model) {
t.Errorf("expected %v, got %v", expectedModel, model)
}
expectedAPIError := &APIError{}
if !reflect.DeepEqual(expectedAPIError, apiError) {
t.Errorf("failureV should be zero valued, exepcted %v, got %v", expectedAPIError, apiError)
}
}

func TestReceive_success(t *testing.T) {
client, mux, server := testServer()
defer server.Close()
Expand Down

0 comments on commit 5ce5b3a

Please sign in to comment.