Skip to content

Commit

Permalink
Reword body handling to use providers
Browse files Browse the repository at this point in the history
Incidentally, also add support for multipart and file uploading.
  • Loading branch information
jawher committed Aug 22, 2016
1 parent 52e88a7 commit 0e3c670
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 108 deletions.
156 changes: 156 additions & 0 deletions body_providers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package sling

import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"strings"

"mime/multipart"
"os"

"path/filepath"

goquery "github.com/google/go-querystring/query"
)

type BodyProvider interface {
ContentType() string
Body() (io.ReadCloser, error)
}

// Public

func ReaderBody(body io.Reader) BodyProvider {
return readerBodyProvider{reader: body}
}

func JSONBody(value interface{}) BodyProvider {
if value == nil {
return nil
}

return jsonBodyProvider{payload: value}
}

func FormBody(value interface{}) BodyProvider {
if value == nil {
return nil
}

return formBodyProvider{payload: value}
}

func MultipartBody(files map[string]string, extraParams map[string]string) BodyProvider {
return multipartBodyProvider{files: files, extraParams: extraParams}
}

// Implementations

// JSON

type jsonBodyProvider struct {
payload interface{}
}

func (p jsonBodyProvider) ContentType() string {
return jsonContentType
}

func (p jsonBodyProvider) Body() (io.ReadCloser, error) {
buf := &bytes.Buffer{}
err := json.NewEncoder(buf).Encode(p.payload)
if err != nil {
return nil, err
}

return ioutil.NopCloser(buf), nil
}

// Form
type formBodyProvider struct {
payload interface{}
}

func (p formBodyProvider) ContentType() string {
return formContentType
}

func (p formBodyProvider) Body() (io.ReadCloser, error) {
values, err := goquery.Values(p.payload)
if err != nil {
return nil, err
}
return ioutil.NopCloser(strings.NewReader(values.Encode())), nil
}

// Reader

type readerBodyProvider struct {
reader io.Reader
}

func (p readerBodyProvider) ContentType() string {
return ""
}

func (p readerBodyProvider) Body() (io.ReadCloser, error) {
rc, ok := p.reader.(io.ReadCloser)
if !ok {
rc = ioutil.NopCloser(p.reader)
}
return rc, nil
}

// Multipart

var (
multipartFromBoundary = "SlingFormBoundary0amF3aGVy"
)

type multipartBodyProvider struct {
files map[string]string
extraParams map[string]string
}

func (p multipartBodyProvider) ContentType() string {
return "multipart/form-data; boundary=" + multipartFromBoundary
}

func (p multipartBodyProvider) Body() (io.ReadCloser, error) {
var body bytes.Buffer
w := multipart.NewWriter(&body)
w.SetBoundary(multipartFromBoundary)

for paramName, filePath := range p.files {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
fw, err := w.CreateFormFile(paramName, filepath.Base(filePath))
if err != nil {
return nil, err
}
if _, err = io.Copy(fw, f); err != nil {
return nil, err
}
}
// Add the other fields
for paramName, paramValue := range p.extraParams {
fw, err := w.CreateFormField(paramName)
if err != nil {
return nil, err
}
if _, err := fw.Write([]byte(paramValue)); err != nil {
return nil, err
}
}

if err := w.Close(); err != nil {
return nil, err
}

return ioutil.NopCloser(&body), nil
}
102 changes: 29 additions & 73 deletions sling.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package sling

import (
"bytes"
"encoding/base64"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -38,12 +36,8 @@ type Sling struct {
header http.Header
// url tagged query structs
queryStructs []interface{}
// json tagged body struct
bodyJSON interface{}
// url tagged body struct (form)
bodyForm interface{}
// simply assigned body
body io.ReadCloser
// body provider
body BodyProvider
}

// New returns a new Sling with an http DefaultClient.
Expand Down Expand Up @@ -80,8 +74,6 @@ func (s *Sling) New() *Sling {
rawURL: s.rawURL,
header: headerCopy,
queryStructs: append([]interface{}{}, s.queryStructs...),
bodyJSON: s.bodyJSON,
bodyForm: s.bodyForm,
body: s.body,
}
}
Expand Down Expand Up @@ -211,43 +203,46 @@ func (s *Sling) QueryStruct(queryStruct interface{}) *Sling {

// Body

func (s *Sling) BodyProvider(body BodyProvider) *Sling {
if body == nil {
return s
}

s.body = body

typ := body.ContentType()
if typ != "" {
s.Set(contentType, typ)
}

return s
}

// BodyJSON sets the Sling's bodyJSON. The value pointed to by the bodyJSON
// will be JSON encoded as the Body on new requests (see Request()).
// The bodyJSON argument should be a pointer to a JSON tagged struct. See
// https://golang.org/pkg/encoding/json/#MarshalIndent for details.
func (s *Sling) BodyJSON(bodyJSON interface{}) *Sling {
if bodyJSON != nil {
s.bodyJSON = bodyJSON
s.Set(contentType, jsonContentType)
}
return s
return s.BodyProvider(JSONBody(bodyJSON))
}

// BodyForm sets the Sling's bodyForm. The value pointed to by the bodyForm
// will be url encoded as the Body on new requests (see Request()).
// 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) BodyForm(bodyForm interface{}) *Sling {
if bodyForm != nil {
s.bodyForm = bodyForm
s.Set(contentType, formContentType)
}
return s
return s.BodyProvider(FormBody(bodyForm))
}

// Body sets the Sling's body. The body value will be set as the Body on new
// requests (see Request()).
// If the provided body is also an io.Closer, the request Body will be closed
// by http.Client methods.
func (s *Sling) Body(body io.Reader) *Sling {
rc, ok := body.(io.ReadCloser)
if !ok && body != nil {
rc = ioutil.NopCloser(body)
}
if rc != nil {
s.body = rc
if body == nil {
return s
}
return s
return s.BodyProvider(ReaderBody(body))
}

// Requests
Expand All @@ -264,9 +259,13 @@ func (s *Sling) Request() (*http.Request, error) {
if err != nil {
return nil, err
}
body, err := s.getRequestBody()
if err != nil {
return nil, err
var body io.Reader = nil

if s.body != nil {
body, err = s.body.Body()
if err != nil {
return nil, err
}
}
req, err := http.NewRequest(s.method, reqURL.String(), body)
if err != nil {
Expand Down Expand Up @@ -301,49 +300,6 @@ 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.bodyJSON != nil && s.header.Get(contentType) == jsonContentType {
body, err = encodeBodyJSON(s.bodyJSON)
if err != nil {
return nil, err
}
} else if s.bodyForm != nil && s.header.Get(contentType) == formContentType {
body, err = encodeBodyForm(s.bodyForm)
if err != nil {
return nil, err
}
} else if s.body != nil {
body = s.body
}
return body, nil
}

// encodeBodyJSON JSON encodes the value pointed to by bodyJSON into an
// io.Reader, typically for use as a Request Body.
func encodeBodyJSON(bodyJSON interface{}) (io.Reader, error) {
var buf = new(bytes.Buffer)
if bodyJSON != nil {
buf = &bytes.Buffer{}
err := json.NewEncoder(buf).Encode(bodyJSON)
if err != nil {
return nil, err
}
}
return buf, nil
}

// encodeBodyForm url encodes the value pointed to by bodyForm into an
// io.Reader, typically for use as a Request Body.
func encodeBodyForm(bodyForm interface{}) (io.Reader, error) {
values, err := goquery.Values(bodyForm)
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
Loading

0 comments on commit 0e3c670

Please sign in to comment.