diff --git a/README.md b/README.md index 9432eb65..aee6f081 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Simple HTTP and REST client for Go inspired by Ruby rest-client. Provides notabl * Choose between HTTP and RESTful mode. Default is RESTful * `HTTP` - default upto 10 redirects and no automatic response unmarshal * `RESTful` - default no redirects and automatic response unmarshal for `JSON` & `XML` -* Client settings like Timeout, RedirectPolicy, Proxy and TLSClientConfig +* Client settings like `Timeout`, `RedirectPolicy`, `Proxy` and `TLSClientConfig` * Client API design * Have client level settings & options and also override at Request level if you want to * [Request](https://godoc.org/github.com/go-resty/resty#Client.OnBeforeRequest) and [Response](https://godoc.org/github.com/go-resty/resty#Client.OnAfterResponse) middleware @@ -38,7 +38,7 @@ resty tested with Go 1.2 and above. * FlexibleRedirectPolicy * DomainCheckRedirectPolicy * etc. [more info](redirect.go) - * Write Cookies to file from CookiesJar (upcoming) + * Persist Cookies into file in JSON format from resty client (upcoming) * etc. ## Installation @@ -104,6 +104,14 @@ resp, err := resty.R(). SetHeader("Accept", "application/json"). SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F"). Get("/search_result") + + +// Sample of using Request.SetQueryString method +resp, err := resty.R(). + SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more"). + SetHeader("Accept", "application/json"). + SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F"). + Get("/show_product") ``` #### Various POST method combinations diff --git a/client.go b/client.go index 807b5324..67406c85 100644 --- a/client.go +++ b/client.go @@ -354,12 +354,7 @@ func (c *Client) SetContentLength(l bool) *Client { // resty.SetError(Error{}) // func (c *Client) SetError(err interface{}) *Client { - t := reflect.TypeOf(err) - if t.Kind() == reflect.Ptr { - c.Error = t.Elem() - } else { - c.Error = t - } + c.Error = getType(err) return c } @@ -463,7 +458,7 @@ func (c *Client) SetTimeout(timeout time.Duration) *Client { c.transport.Dial = func(network, addr string) (net.Conn, error) { conn, err := net.DialTimeout(network, addr, timeout) if err != nil { - c.Log.Printf("Error: %v", err) + c.Log.Printf("ERROR [%v]", err) return nil, err } conn.SetDeadline(time.Now().Add(timeout)) @@ -484,7 +479,7 @@ func (c *Client) SetProxy(proxyURL string) *Client { if pURL, err := url.Parse(proxyURL); err == nil { c.transport.Proxy = http.ProxyURL(pURL) } else { - c.Log.Printf("ERROR: %v", err) + c.Log.Printf("ERROR [%v]", err) } return c @@ -643,6 +638,24 @@ func (r *Request) SetQueryParams(params map[string]string) *Request { return r } +// SetQueryString method provides ability to use string as an input to set URL query string for the request. +// +// Using String as an input +// resty.R(). +// SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more") +// +func (r *Request) SetQueryString(query string) *Request { + values, err := url.ParseQuery(strings.TrimSpace(query)) + if err == nil { + for p, _ := range values { + r.QueryParam.Add(p, values.Get(p)) + } + } else { + r.client.Log.Printf("ERROR [%v]", err) + } + return r +} + // SetFormData method sets Form parameters and its values in the current request. // It's applicable only HTTP method `POST` and `PUT` and requets content type would be set as // `application/x-www-form-urlencoded`. @@ -934,9 +947,7 @@ func IsStringEmpty(str string) bool { // DetectContentType method is used to figure out `Request.Body` content type for request header func DetectContentType(body interface{}) string { contentType := plainTextType - kind := reflect.ValueOf(body).Kind() - - switch kind { + switch getBaseKind(body) { case reflect.Struct, reflect.Map: contentType = jsonContentType case reflect.String: @@ -970,14 +981,7 @@ func Unmarshal(ct string, b []byte, d interface{}) (err error) { } func getLogger(w io.Writer) *log.Logger { - var l *log.Logger - if w == nil { - l = log.New(os.Stderr, "RESTY ", log.LstdFlags) - } else { - l = log.New(w, "RESTY ", log.LstdFlags) - } - - return l + return log.New(w, "RESTY ", log.LstdFlags) } func addFile(w *multipart.Writer, fieldName, path string) error { @@ -998,11 +1002,10 @@ func addFile(w *multipart.Writer, fieldName, path string) error { func getRequestBodyString(r *Request) (body string) { body = "***** NO CONTENT *****" - if r.Method == POST || r.Method == PUT || r.Method == PATCH { + if isPayloadSupported(r.Method) { // multipart/form-data OR form data if r.isMultiPart || r.isFormData { body = string(r.bodyBuf.Bytes()) - return } @@ -1062,10 +1065,25 @@ func getResponseBodyString(res *Response) string { } func getPointer(v interface{}) interface{} { - rv := reflect.TypeOf(v) - if rv.Kind() != reflect.Ptr { - return reflect.New(rv).Interface() + vv := reflect.ValueOf(v) + if vv.Kind() == reflect.Ptr { + return v } + return reflect.New(vv.Type()).Interface() +} - return v +func isPayloadSupported(m string) bool { + return (m == POST || m == PUT || m == DELETE || m == PATCH) +} + +func getBaseKind(v interface{}) reflect.Kind { + return getType(v).Kind() +} + +func getType(v interface{}) reflect.Type { + vv := reflect.ValueOf(v) + if vv.Kind() == reflect.Ptr { + return vv.Elem().Type() + } + return vv.Type() } diff --git a/default.go b/default.go index bc5616b4..cbbcf5a7 100644 --- a/default.go +++ b/default.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/cookiejar" "net/url" + "os" "time" "golang.org/x/net/publicsuffix" @@ -39,7 +40,7 @@ func New() *Client { Token: "", Cookies: make([]*http.Cookie, 0), Debug: false, - Log: getLogger(nil), + Log: getLogger(os.Stderr), httpClient: &http.Client{Jar: cookieJar}, transport: &http.Transport{}, } @@ -72,122 +73,122 @@ func R() *Request { return DefaultClient.R() } -// SetHostURL sets Host URL. See Client.SetHostURL for more information. +// SetHostURL sets Host URL. See `Client.SetHostURL for more information. func SetHostURL(url string) *Client { return DefaultClient.SetHostURL(url) } -// SetHeader sets single header. See Client.SetHeader for more information. +// SetHeader sets single header. See `Client.SetHeader` for more information. func SetHeader(header, value string) *Client { return DefaultClient.SetHeader(header, value) } -// SetHeaders sets multiple headers. See Client.SetHeaders for more information. +// SetHeaders sets multiple headers. See `Client.SetHeaders` for more information. func SetHeaders(headers map[string]string) *Client { return DefaultClient.SetHeaders(headers) } -// SetCookie sets single cookie object. See Client.SetCookie for more information. +// SetCookie sets single cookie object. See `Client.SetCookie` for more information. func SetCookie(hc *http.Cookie) *Client { return DefaultClient.SetCookie(hc) } -// SetCookies sets multiple cookie object. See Client.SetCookies for more information. +// SetCookies sets multiple cookie object. See `Client.SetCookies` for more information. func SetCookies(cs []*http.Cookie) *Client { return DefaultClient.SetCookies(cs) } -// SetQueryParam method sets single paramater and its value. See Client.SetQueryParam for more information. +// SetQueryParam method sets single paramater and its value. See `Client.SetQueryParam` for more information. func SetQueryParam(param, value string) *Client { return DefaultClient.SetQueryParam(param, value) } -// SetQueryParams method sets multiple paramaters and its value. See Client.SetQueryParams for more information. +// SetQueryParams method sets multiple paramaters and its value. See `Client.SetQueryParams` for more information. func SetQueryParams(params map[string]string) *Client { return DefaultClient.SetQueryParams(params) } -// SetFormData method sets Form parameters and its values. See Client.SetFormData for more information. +// SetFormData method sets Form parameters and its values. See `Client.SetFormData` for more information. func SetFormData(data map[string]string) *Client { return DefaultClient.SetFormData(data) } -// SetBasicAuth method sets the basic authentication header. See Client.SetBasicAuth for more information. +// SetBasicAuth method sets the basic authentication header. See `Client.SetBasicAuth` for more information. func SetBasicAuth(username, password string) *Client { return DefaultClient.SetBasicAuth(username, password) } -// SetAuthToken method sets bearer auth token header. See Client.SetAuthToken for more information. +// SetAuthToken method sets bearer auth token header. See `Client.SetAuthToken` for more information. func SetAuthToken(token string) *Client { return DefaultClient.SetAuthToken(token) } -// OnBeforeRequest method sets request middleware. See Client.OnBeforeRequest for more information. +// OnBeforeRequest method sets request middleware. See `Client.OnBeforeRequest` for more information. func OnBeforeRequest(m func(*Client, *Request) error) *Client { return DefaultClient.OnBeforeRequest(m) } -// OnAfterResponse method sets response middleware. See Client.OnAfterResponse for more information. +// OnAfterResponse method sets response middleware. See `Client.OnAfterResponse` for more information. func OnAfterResponse(m func(*Client, *Response) error) *Client { return DefaultClient.OnAfterResponse(m) } -// SetDebug method enables the debug mode. See Client.SetDebug for more information. +// SetDebug method enables the debug mode. See `Client.SetDebug` for more information. func SetDebug(d bool) *Client { return DefaultClient.SetDebug(d) } -// SetLogger method sets given writer for logging. See Client.SetLogger for more information. +// SetLogger method sets given writer for logging. See `Client.SetLogger` for more information. func SetLogger(w io.Writer) *Client { return DefaultClient.SetLogger(w) } -// SetContentLength method enables `Content-Length` value. See Client.SetContentLength for more information. +// SetContentLength method enables `Content-Length` value. See `Client.SetContentLength` for more information. func SetContentLength(l bool) *Client { return DefaultClient.SetContentLength(l) } -// SetError method is to register the global or client common `Error` object. See Client.SetError for more information. +// SetError method is to register the global or client common `Error` object. See `Client.SetError` for more information. func SetError(err interface{}) *Client { return DefaultClient.SetError(err) } -// SetRedirectPolicy method sets the client redirect poilicy. See Client.SetRedirectPolicy for more information. +// SetRedirectPolicy method sets the client redirect poilicy. See `Client.SetRedirectPolicy` for more information. func SetRedirectPolicy(policies ...interface{}) *Client { return DefaultClient.SetRedirectPolicy(policies...) } -// SetHTTPMode method sets go-resty mode into HTTP. See Client.SetMode for more information. +// SetHTTPMode method sets go-resty mode into HTTP. See `Client.SetMode` for more information. func SetHTTPMode() *Client { return DefaultClient.SetHTTPMode() } -// SetRESTMode method sets go-resty mode into RESTful. See Client.SetMode for more information. +// SetRESTMode method sets go-resty mode into RESTful. See `Client.SetMode` for more information. func SetRESTMode() *Client { return DefaultClient.SetRESTMode() } -// Mode method returns the current client mode. See Client.Mode for more information. +// Mode method returns the current client mode. See `Client.Mode` for more information. func Mode() string { return DefaultClient.Mode() } -// SetTLSClientConfig method sets TLSClientConfig for underling client Transport. See Client.SetTLSClientConfig for more information. +// SetTLSClientConfig method sets TLSClientConfig for underling client Transport. See `Client.SetTLSClientConfig` for more information. func SetTLSClientConfig(config *tls.Config) *Client { return DefaultClient.SetTLSClientConfig(config) } -// SetTimeout method sets timeout for request. See Client.SetTimeout for more information. +// SetTimeout method sets timeout for request. See `Client.SetTimeout` for more information. func SetTimeout(timeout time.Duration) *Client { return DefaultClient.SetTimeout(timeout) } -// SetProxy method sets Proxy for request. See Client.SetProxy for more information. +// SetProxy method sets Proxy for request. See `Client.SetProxy` for more information. func SetProxy(proxyURL string) *Client { return DefaultClient.SetProxy(proxyURL) } -// RemoveProxy method removes the proxy configuration. See Client.RemoveProxy for more information. +// RemoveProxy method removes the proxy configuration. See `Client.RemoveProxy` for more information. func RemoveProxy() *Client { return DefaultClient.RemoveProxy() } diff --git a/middleware.go b/middleware.go index 92e6f0e7..73a6c8ab 100644 --- a/middleware.go +++ b/middleware.go @@ -81,7 +81,7 @@ func parseRequestHeader(c *Client, r *Request) error { } func parseRequestBody(c *Client, r *Request) (err error) { - if r.Method == POST || r.Method == PUT || r.Method == PATCH { + if isPayloadSupported(r.Method) { // Handling Multipart if r.isMultiPart && !(r.Method == PATCH) { r.bodyBuf = &bytes.Buffer{} @@ -136,11 +136,7 @@ func parseRequestBody(c *Client, r *Request) (err error) { } var bodyBytes []byte - kind := reflect.ValueOf(r.Body).Kind() - if kind == reflect.Ptr { - kind = reflect.TypeOf(r.Body).Elem().Kind() - } - + kind := getBaseKind(r.Body) if IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map) { bodyBytes, err = json.Marshal(r.Body) } else if IsXMLType(contentType) && (kind == reflect.Struct) { diff --git a/resty.go b/resty.go index 8b4efe1a..92d42fdc 100644 --- a/resty.go +++ b/resty.go @@ -9,4 +9,4 @@ Package resty provides simple HTTP and REST client for Go inspired by Ruby rest- package resty // go-resty version no -var Version = "0.2.3" +var Version = "0.3" diff --git a/resty_test.go b/resty_test.go index 7a527453..5a68e3df 100644 --- a/resty_test.go +++ b/resty_test.go @@ -492,6 +492,28 @@ func TestMultiPartUploadFile(t *testing.T) { assertEqual(t, http.StatusOK, resp.StatusCode()) } +func TestMultiPartUploadFileError(t *testing.T) { + ts := createFormPostServer(t) + defer ts.Close() + + pwd, _ := os.Getwd() + basePath := pwd + "/test-data" + + c := dc() + c.SetFormData(map[string]string{"zip_code": "00001", "city": "Los Angeles"}) + + resp, err := c.R(). + SetFile("profile_img", basePath+"/test-img-not-exists.png"). + Post(ts.URL + "/upload") + + if err == nil { + t.Errorf("Expected [%v], got [%v]", nil, err) + } + if resp != nil { + t.Errorf("Expected [%v], got [%v]", nil, resp) + } +} + func TestMultiPartUploadFiles(t *testing.T) { ts := createFormPostServer(t) defer ts.Close() @@ -747,6 +769,14 @@ func TestClientTimeout(t *testing.T) { assertEqual(t, true, strings.Contains(err.Error(), "i/o timeout")) } +func TestClientTimeoutInternalError(t *testing.T) { + c := dc() + c.SetHTTPMode() + c.SetTimeout(time.Duration(time.Second * 1)) + + c.R().Get("http://localhost:9000/set-timeout-test") +} + func TestHeadMethod(t *testing.T) { ts := createGetServer(t) defer ts.Close() @@ -824,6 +854,53 @@ func TestIncorrectURL(t *testing.T) { assertEqual(t, true, strings.Contains(err1.Error(), "parse //not.a.user@%66%6f%6f.com/just/a/path/also")) } +func TestDetectContentTypeForPointer(t *testing.T) { + ts := createPostServer(t) + defer ts.Close() + + user := &User{Username: "testuser", Password: "testpass"} + + resp, err := dclr(). + SetBody(user). + SetResult(AuthSuccess{}). + Post(ts.URL + "/login") + + assertError(t, err) + assertEqual(t, http.StatusOK, resp.StatusCode()) + + t.Logf("Result Success: %q", resp.Result().(*AuthSuccess)) + + logResponse(t, resp) +} + +func TestSetQueryStringTypical(t *testing.T) { + ts := createGetServer(t) + defer ts.Close() + + resp, err := dclr(). + SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more"). + Get(ts.URL) + + assertError(t, err) + assertEqual(t, http.StatusOK, resp.StatusCode()) + assertEqual(t, "200 OK", resp.Status()) + assertEqual(t, "TestGet: text response", resp.String()) +} + +func TestSetQueryStringTypicalError(t *testing.T) { + ts := createGetServer(t) + defer ts.Close() + + resp, err := dclr(). + SetQueryString("&%%amp;"). + Get(ts.URL) + + assertError(t, err) + assertEqual(t, http.StatusOK, resp.StatusCode()) + assertEqual(t, "200 OK", resp.Status()) + assertEqual(t, "TestGet: text response", resp.String()) +} + func TestClientOptions(t *testing.T) { SetHTTPMode().SetContentLength(true) assertEqual(t, Mode(), "http")