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

Introduce response decoder interface #49

Merged
merged 1 commit into from
Nov 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package sling

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

// ResponseDecoder decodes http responses into struct values.
// Decode decodes the response into the value pointed to by v.
type ResponseDecoder interface {
Decode(resp *http.Response, v interface{}) error
maxbeutel marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 {
maxbeutel marked this conversation as resolved.
Show resolved Hide resolved
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