Skip to content

Commit

Permalink
Add BodyStruct setter which adds a url encoded struct form to Body
Browse files Browse the repository at this point in the history
* BodyStruct mirrors JsonBody except the struct is encoded by url tags as
a form and the Content-Type is application/x-www-form-urlencoded
  • Loading branch information
dghubble committed Apr 21, 2015
1 parent 3bbc4d3 commit 748e0f2
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 76 deletions.
53 changes: 47 additions & 6 deletions sling.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/url"
"strings"
)

const (
Expand All @@ -18,6 +19,7 @@ const (
DELETE = "DELETE"
contentType = "Content-Type"
jsonContentType = "application/json"
formContentType = "application/x-www-form-urlencoded"
)

// Sling is an HTTP Request builder and sender.
Expand All @@ -34,6 +36,8 @@ type Sling struct {
queryStructs []interface{}
// json tagged body struct
jsonBody interface{}
// url tagged body struct (form)
bodyStruct interface{}
}

// New returns a new Sling with an http DefaultClient.
Expand Down Expand Up @@ -71,6 +75,7 @@ func (s *Sling) New() *Sling {
Header: headerCopy,
queryStructs: append([]interface{}{}, s.queryStructs...),
jsonBody: s.jsonBody,
bodyStruct: s.bodyStruct,
}
}

Expand Down Expand Up @@ -188,6 +193,18 @@ func (s *Sling) JsonBody(jsonBody interface{}) *Sling {
return s
}

// BodyStruct sets the Sling's bodyStruct. The value pointed to by the
// bodyStruct will be url encoded to set the Body on new requests.
// The bodyStruct argument should be a pointer to a url tagged struct. See
// https://godoc.org/github.com/google/go-querystring/query for details.
func (s *Sling) BodyStruct(bodyStruct interface{}) *Sling {
if bodyStruct != nil {
s.bodyStruct = bodyStruct
s.Set(contentType, formContentType)
}
return s
}

// Requests

// Request returns a new http.Request created with the Sling properties.
Expand All @@ -202,12 +219,9 @@ func (s *Sling) Request() (*http.Request, error) {
if err != nil {
return nil, err
}
var body io.Reader
if s.jsonBody != nil {
body, err = encodeJsonBody(s.jsonBody)
if err != nil {
return nil, err
}
body, err := s.getRequestBody()
if err != nil {
return nil, err
}
req, err := http.NewRequest(s.Method, reqURL.String(), body)
if err != nil {
Expand Down Expand Up @@ -242,6 +256,23 @@ func addQueryStructs(reqURL *url.URL, queryStructs []interface{}) error {
return nil
}

// getRequestBody returns the io.Reader which should be used as the body
// of new Requests.
func (s *Sling) getRequestBody() (body io.Reader, err error) {
if s.jsonBody != nil && s.Header.Get(contentType) == jsonContentType {
body, err = encodeJsonBody(s.jsonBody)
if err != nil {
return nil, err
}
} else if s.bodyStruct != nil && s.Header.Get(contentType) == formContentType {
body, err = encodeBodyStruct(s.bodyStruct)
if err != nil {
return nil, err
}
}
return body, nil
}

// encodeJsonBody JSON encodes the value pointed to by jsonBody into an
// io.Reader, typically for use as a Request Body.
func encodeJsonBody(jsonBody interface{}) (io.Reader, error) {
Expand All @@ -256,6 +287,16 @@ func encodeJsonBody(jsonBody interface{}) (io.Reader, error) {
return buf, nil
}

// encodeBodyStruct url encodes the value pointed to by bodyStruct into an
// io.Reader, typically for use as a Request Body.
func encodeBodyStruct(bodyStruct interface{}) (io.Reader, error) {
values, err := goquery.Values(bodyStruct)
if err != nil {
return nil, err
}
return strings.NewReader(values.Encode()), nil
}

// addHeaders adds the key, value pairs from the given http.Header to the
// request. Values for existing keys are appended to the keys values.
func addHeaders(req *http.Request, header http.Header) {
Expand Down
164 changes: 94 additions & 70 deletions sling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,18 @@ import (
"testing"
)

type FakeParams struct {
KindName string `url:"kind_name"`
Count int `url:"count"`
}

// Url-tagged query struct
var paramsA = struct {
Limit int `url:"limit"`
}{
30,
}
var paramsB = struct {
KindName string `url:"kind_name"`
Count int `url:"count"`
}{
"recent",
25,
}
var paramsB = FakeParams{KindName: "recent", Count: 25}

// Json-tagged model struct
type FakeModel struct {
Expand All @@ -34,6 +33,8 @@ type FakeModel struct {
Temperature float64 `json:"temperature,omitempty"`
}

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

func TestNew(t *testing.T) {
sling := New()
if sling.HttpClient != http.DefaultClient {
Expand All @@ -60,6 +61,8 @@ func TestSlingNew(t *testing.T) {
New().Add("Content-Type", "application/json"),
New().Add("A", "B").Add("a", "c").New(),
New().Add("A", "B").New().Add("a", "c"),
New().BodyStruct(paramsB),
New().BodyStruct(paramsB).New(),
}
for _, sling := range cases {
child := sling.New()
Expand Down Expand Up @@ -96,6 +99,10 @@ func TestSlingNew(t *testing.T) {
if child.jsonBody != sling.jsonBody {
t.Errorf("expected %v, got %v")
}
// bodyStruct should be copied
if child.bodyStruct != sling.bodyStruct {
t.Errorf("expected %v, got %v", sling.bodyStruct, child.bodyStruct)
}
}
}

Expand Down Expand Up @@ -259,8 +266,12 @@ func TestJsonBodySetter(t *testing.T) {
input interface{}
expected interface{}
}{
{fakeModel, nil, fakeModel},
// json tagged struct is set as jsonBody
{nil, fakeModel, fakeModel},
// nil argument to jsonBody does not replace existing jsonBody
{fakeModel, nil, fakeModel},
// nil jsonBody remains nil
{nil, nil, nil},
}
for _, c := range cases {
sling := New()
Expand All @@ -278,6 +289,36 @@ func TestJsonBodySetter(t *testing.T) {
}
}

func TestBodyStructSetter(t *testing.T) {
cases := []struct {
initial interface{}
input interface{}
expected interface{}
}{
// url tagged struct is set as bodyStruct
{nil, paramsB, paramsB},
// nil argument to bodyStruct does not replace existing bodyStruct
{paramsB, nil, paramsB},
// nil bodyStruct remains nil
{nil, nil, nil},
}
for _, c := range cases {
sling := New()
sling.bodyStruct = c.initial
sling.BodyStruct(c.input)
if sling.bodyStruct != c.expected {
t.Errorf("expected %v, got %v", c.expected, sling.bodyStruct)
}
// Content-Type should be application/x-www-form-urlencoded if bodyStruct was non-nil
if c.input != nil && sling.Header.Get(contentType) != formContentType {
t.Errorf("Incorrect or missing header, expected %s, got %s", formContentType, sling.Header.Get(contentType))
} else if c.input == nil && sling.Header.Get(contentType) != "" {
t.Errorf("did not expect a Content-Type header, got %s", sling.Header.Get(contentType))
}
}

}

func TestRequest_urlAndMethod(t *testing.T) {
cases := []struct {
sling *Sling
Expand Down Expand Up @@ -338,17 +379,30 @@ func TestRequest_queryStructs(t *testing.T) {
}
}

func TestRequest_jsonBody(t *testing.T) {
func TestRequest_body(t *testing.T) {
cases := []struct {
sling *Sling
expectedBody string // expected Body io.Reader as a string
sling *Sling
expectedBody string // expected Body io.Reader as a string
expectedContentType string
}{
{New().JsonBody(&FakeModel{Text: "note", FavoriteCount: 12}), "{\"text\":\"note\",\"favorite_count\":12}\n"},
{New().JsonBody(FakeModel{Text: "note", FavoriteCount: 12}), "{\"text\":\"note\",\"favorite_count\":12}\n"},
{New().JsonBody(&FakeModel{}), "{}\n"},
{New().JsonBody(FakeModel{}), "{}\n"},
// setting the jsonBody overrides existing jsonBody
{New().JsonBody(&FakeModel{}).JsonBody(&FakeModel{Text: "msg"}), "{\"text\":\"msg\"}\n"},
// JsonBody
{New().JsonBody(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
{New().JsonBody(&modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
{New().JsonBody(&FakeModel{}), "{}\n", jsonContentType},
{New().JsonBody(FakeModel{}), "{}\n", jsonContentType},
// JsonBody overrides existing values
{New().JsonBody(&FakeModel{}).JsonBody(&FakeModel{Text: "msg"}), "{\"text\":\"msg\"}\n", jsonContentType},
// BodyStruct (form)
{New().BodyStruct(paramsA), "limit=30", formContentType},
{New().BodyStruct(paramsB), "count=25&kind_name=recent", formContentType},
{New().BodyStruct(&paramsB), "count=25&kind_name=recent", formContentType},
// BodyStruct overrides existing values
{New().BodyStruct(paramsA).New().BodyStruct(paramsB), "count=25&kind_name=recent", formContentType},
// Mixture of JsonBody and BodyStruct prefers body setter called last with a non-nil argument
{New().BodyStruct(paramsB).New().JsonBody(modelA), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
{New().JsonBody(modelA).New().BodyStruct(paramsB), "count=25&kind_name=recent", formContentType},
{New().BodyStruct(paramsB).New().JsonBody(nil), "count=25&kind_name=recent", formContentType},
{New().JsonBody(modelA).New().BodyStruct(nil), "{\"text\":\"note\",\"favorite_count\":12}\n", jsonContentType},
}
for _, c := range cases {
req, _ := c.sling.Request()
Expand All @@ -359,15 +413,18 @@ func TestRequest_jsonBody(t *testing.T) {
t.Errorf("expected Request.Body %s, got %s", c.expectedBody, value)
}
// Header Content-Type should be application/json
if actualHeader := req.Header.Get(contentType); actualHeader != jsonContentType {
t.Errorf("Incorrect or missing header, expected %s, got %s", jsonContentType, actualHeader)
if actualHeader := req.Header.Get(contentType); actualHeader != c.expectedContentType {
t.Errorf("Incorrect or missing header, expected %s, got %s", c.expectedContentType, actualHeader)
}
}
}

// test that Body is left nil when no JSON struct is set via JsonBody
func TestRequest_bodyNoData(t *testing.T) {
// test that Body is left nil when no jsonBody or bodyStruct set
slings := []*Sling{
New().JsonBody(nil),
New(),
New().JsonBody(nil),
New().BodyStruct(nil),
}
for _, sling := range slings {
req, _ := sling.Request()
Expand All @@ -379,16 +436,24 @@ func TestRequest_jsonBody(t *testing.T) {
t.Errorf("did not expect a Content-Type header, got %s", actualHeader)
}
}
}

// test that expected jsonBody encoding errors occur, use illegal JSON field
sling := New().JsonBody(&FakeModel{Temperature: math.Inf(1)})
req, err := sling.Request()
expectedErr := errors.New("json: unsupported value: +Inf")
if err == nil || err.Error() != expectedErr.Error() {
t.Errorf("expected error %v, got %v", expectedErr, err)
func TestRequest_bodyEncodeErrors(t *testing.T) {
cases := []struct {
sling *Sling
expectedErr error
}{
// check that Encode errors are propagated, illegal JSON field
{New().JsonBody(FakeModel{Temperature: math.Inf(1)}), errors.New("json: unsupported value: +Inf")},
}
if req != nil {
t.Errorf("expected nil Request, got %v", req)
for _, c := range cases {
req, err := c.sling.Request()
if err == nil || err.Error() != c.expectedErr.Error() {
t.Errorf("expected error %v, got %v", c.expectedErr, err)
}
if req != nil {
t.Errorf("expected nil Request, got %+v", req)
}
}
}

Expand Down Expand Up @@ -444,47 +509,6 @@ func TestAddQueryStructs(t *testing.T) {
}
}

func TestEncodeJsonBody(t *testing.T) {
cases := []struct {
jsonStruct interface{}
expectedReader string // expected io.Reader as a string
expectedErr error
}{
{&FakeModel{Text: "note", FavoriteCount: 12}, "{\"text\":\"note\",\"favorite_count\":12}\n", nil},
{FakeModel{Text: "note", FavoriteCount: 12}, "{\"text\":\"note\",\"favorite_count\":12}\n", nil},
// nil argument should return an empty reader
{nil, "", nil},
// zero valued json-tagged should return empty object JSON {}
{&FakeModel{}, "{}\n", nil},
{FakeModel{}, "{}\n", nil},
// check that Encode errors are propagated, illegal JSON field
{FakeModel{Temperature: math.Inf(1)}, "", errors.New("json: unsupported value: +Inf")},
}
for _, c := range cases {
reader, err := encodeJsonBody(c.jsonStruct)
if c.expectedErr == nil {
// err expected to be nil, io.Reader should be readable
if err != nil {
t.Errorf("expected error %v, got %v", nil, err)
}
buf := new(bytes.Buffer)
buf.ReadFrom(reader)
if value := buf.String(); value != c.expectedReader {
fmt.Println(len(value))
t.Errorf("expected jsonBody string \"%s\", got \"%s\"", c.expectedReader, value)
}
} else {
// err is non-nil, io.Reader is not readable
if err.Error() != c.expectedErr.Error() {
t.Errorf("expected error %s, got %s", c.expectedErr.Error(), err.Error())
}
if reader != nil {
t.Errorf("expected jsonBody nil, got %v", reader)
}
}
}
}

func TestDo(t *testing.T) {
expectedText := "Some text"
var expectedFavoriteCount int64 = 24
Expand Down

0 comments on commit 748e0f2

Please sign in to comment.