diff --git a/client/httplib/client_option.go b/client/httplib/client_option.go index f38c50dc..e7402b8c 100644 --- a/client/httplib/client_option.go +++ b/client/httplib/client_option.go @@ -21,70 +21,62 @@ import ( "time" ) -type ClientOption func(client *Client) error -type BeegoHttpRequestOption func(request *BeegoHTTPRequest) error +type ClientOption func(client *Client) +type BeegoHttpRequestOption func(request *BeegoHTTPRequest) // WithEnableCookie will enable cookie in all subsequent request func WithEnableCookie(enable bool) ClientOption { - return func(client *Client) error { + return func(client *Client) { client.Setting.EnableCookie = enable - return nil } } // WithEnableCookie will adds UA in all subsequent request func WithUserAgent(userAgent string) ClientOption { - return func(client *Client) error { + return func(client *Client) { client.Setting.UserAgent = userAgent - return nil } } // WithTLSClientConfig will adds tls config in all subsequent request func WithTLSClientConfig(config *tls.Config) ClientOption { - return func(client *Client) error { + return func(client *Client) { client.Setting.TLSClientConfig = config - return nil } } // WithTransport will set transport field in all subsequent request func WithTransport(transport http.RoundTripper) ClientOption { - return func(client *Client) error { + return func(client *Client) { client.Setting.Transport = transport - return nil } } // WithProxy will set http proxy field in all subsequent request func WithProxy(proxy func(*http.Request) (*url.URL, error)) ClientOption { - return func(client *Client) error { + return func(client *Client) { client.Setting.Proxy = proxy - return nil } } // WithCheckRedirect will specifies the policy for handling redirects in all subsequent request func WithCheckRedirect(redirect func(req *http.Request, via []*http.Request) error) ClientOption { - return func(client *Client) error { + return func(client *Client) { client.Setting.CheckRedirect = redirect - return nil } } // WithHTTPSetting can replace beegoHTTPSeting func WithHTTPSetting(setting BeegoHTTPSettings) ClientOption { - return func(client *Client) error { - client.Setting = &setting - return nil + return func(client *Client) { + client.Setting = setting } } // WithEnableGzip will enable gzip in all subsequent request func WithEnableGzip(enable bool) ClientOption { - return func(client *Client) error { + return func(client *Client) { client.Setting.Gzip = enable - return nil } } @@ -92,73 +84,60 @@ func WithEnableGzip(enable bool) ClientOption { // WithTimeout sets connect time out and read-write time out for BeegoRequest. func WithTimeout(connectTimeout, readWriteTimeout time.Duration) BeegoHttpRequestOption { - return func(request *BeegoHTTPRequest) error { + return func(request *BeegoHTTPRequest) { request.SetTimeout(connectTimeout, readWriteTimeout) - return nil } } // WithHeader adds header item string in request. func WithHeader(key, value string) BeegoHttpRequestOption { - return func(request *BeegoHTTPRequest) error { + return func(request *BeegoHTTPRequest) { request.Header(key, value) - return nil } } // WithCookie adds a cookie to the request. func WithCookie(cookie *http.Cookie) BeegoHttpRequestOption { - return func(request *BeegoHTTPRequest) error { + return func(request *BeegoHTTPRequest) { request.Header("Cookie", cookie.String()) - return nil } } // Withtokenfactory adds a custom function to set Authorization -func WithTokenFactory(tokenFactory func() (string, error)) BeegoHttpRequestOption { - return func(request *BeegoHTTPRequest) error { - t, err := tokenFactory() - if err != nil { - return err - } +func WithTokenFactory(tokenFactory func() string) BeegoHttpRequestOption { + return func(request *BeegoHTTPRequest) { + t := tokenFactory() + request.Header("Authorization", t) - return nil } } // WithBasicAuth adds a custom function to set basic auth -func WithBasicAuth(basicAuth func() (string, string, error)) BeegoHttpRequestOption { - return func(request *BeegoHTTPRequest) error { - username, password, err := basicAuth() - if err != nil { - return err - } +func WithBasicAuth(basicAuth func() (string, string)) BeegoHttpRequestOption { + return func(request *BeegoHTTPRequest) { + username, password := basicAuth() request.SetBasicAuth(username, password) - return nil } } // WithFilters will use the filter as the invocation filters func WithFilters(fcs ...FilterChain) BeegoHttpRequestOption { - return func(request *BeegoHTTPRequest) error { + return func(request *BeegoHTTPRequest) { request.SetFilters(fcs...) - return nil } } // WithContentType adds ContentType in header func WithContentType(contentType string) BeegoHttpRequestOption { - return func(request *BeegoHTTPRequest) error { - request.Header("Content-Type", contentType) - return nil + return func(request *BeegoHTTPRequest) { + request.Header(contentTypeKey, contentType) } } // WithParam adds query param in to request. func WithParam(key, value string) BeegoHttpRequestOption { - return func(request *BeegoHTTPRequest) error { + return func(request *BeegoHTTPRequest) { request.Param(key, value) - return nil } } @@ -167,9 +146,8 @@ func WithParam(key, value string) BeegoHttpRequestOption { // -1 retry indefinitely (forever) // Other numbers specify the exact retry amount func WithRetry(times int, delay time.Duration) BeegoHttpRequestOption { - return func(request *BeegoHTTPRequest) error { + return func(request *BeegoHTTPRequest) { request.Retries(times) request.RetryDelay(delay) - return nil } } diff --git a/client/httplib/client_option_test.go b/client/httplib/client_option_test.go index d989bd94..0598206c 100644 --- a/client/httplib/client_option_test.go +++ b/client/httplib/client_option_test.go @@ -147,8 +147,8 @@ func TestOption_WithTokenFactory(t *testing.T) { t.Fatal(err) } client.CommonOpts = append(client.CommonOpts, - WithTokenFactory(func() (string, error) { - return "testauth", nil + WithTokenFactory(func() string { + return "testauth" })) var str *string @@ -172,8 +172,8 @@ func TestOption_WithBasicAuth(t *testing.T) { var str *string err = client.Get(&str, "/basic-auth/user/passwd", - WithBasicAuth(func() (string, string, error) { - return "user", "passwd", nil + WithBasicAuth(func() (string, string) { + return "user", "passwd" })) if err != nil { t.Fatal(err) @@ -240,7 +240,9 @@ func TestOption_WithRetry(t *testing.T) { err = client.Get(nil, "", WithRetry(retryAmount, retryDelay)) - assert.NotNil(t, err) + if err != nil { + t.Fatal(err) + } endTime := time.Now().UnixNano() / int64(time.Millisecond) elapsedTime := endTime - startTime delayedTime := int64(retryAmount) * retryDelay.Milliseconds() diff --git a/client/httplib/httpclient.go b/client/httplib/httpclient.go index ea6e4316..4cfadedf 100644 --- a/client/httplib/httpclient.go +++ b/client/httplib/httpclient.go @@ -16,9 +16,6 @@ package httplib import ( "net/http" - "strings" - - "github.com/beego/beego/v2/core/berror" ) // Client provides an HTTP client supporting chain call @@ -27,8 +24,8 @@ type Client struct { Endpoint string CommonOpts []BeegoHttpRequestOption - Setting *BeegoHTTPSettings - pointer *responsePointer + Setting BeegoHTTPSettings + pointer responsePointer } type responsePointer struct { @@ -46,71 +43,42 @@ func NewClient(name string, endpoint string, opts ...ClientOption) (*Client, err Endpoint: endpoint, } setting := GetDefaultSetting() - res.Setting = &setting + res.Setting = setting for _, o := range opts { - err := o(res) - if err != nil { - return nil, err - } + o(res) } return res, nil } // Response will set response to the pointer func (c *Client) Response(resp **http.Response) *Client { - if c.pointer == nil { - newC := *c - newC.pointer = &responsePointer{ - response: resp, - } - return &newC - } - c.pointer.response = resp - return c + newC := *c + newC.pointer.response = resp + return &newC } // StatusCode will set response StatusCode to the pointer func (c *Client) StatusCode(code **int) *Client { - if c.pointer == nil { - newC := *c - newC.pointer = &responsePointer{ - statusCode: code, - } - return &newC - } - c.pointer.statusCode = code - return c + newC := *c + newC.pointer.statusCode = code + return &newC } // Headers will set response Headers to the pointer func (c *Client) Headers(headers **http.Header) *Client { - if c.pointer == nil { - newC := *c - newC.pointer = &responsePointer{ - header: headers, - } - return &newC - } - c.pointer.header = headers - return c + newC := *c + newC.pointer.header = headers + return &newC } // HeaderValue will set response HeaderValue to the pointer func (c *Client) HeaderValue(key string, value **string) *Client { - if c.pointer == nil { - newC := *c - newC.pointer = &responsePointer{ - headerValues: map[string]**string{ - key: value, - }, - } - return &newC + newC := *c + if newC.pointer.headerValues == nil { + newC.pointer.headerValues = make(map[string]**string) } - if c.pointer.headerValues == nil { - c.pointer.headerValues = map[string]**string{} - } - c.pointer.headerValues[key] = value - return c + newC.pointer.headerValues[key] = value + return &newC } // ContentType will set response ContentType to the pointer @@ -120,22 +88,13 @@ func (c *Client) ContentType(contentType **string) *Client { // ContentLength will set response ContentLength to the pointer func (c *Client) ContentLength(contentLength **int64) *Client { - if c.pointer == nil { - newC := *c - newC.pointer = &responsePointer{ - contentLength: contentLength, - } - return &newC - } - c.pointer.contentLength = contentLength - return c + newC := *c + newC.pointer.contentLength = contentLength + return &newC } // setPointers set the http response value to pointer func (c *Client) setPointers(resp *http.Response) { - if c.pointer == nil { - return - } if c.pointer.response != nil { *c.pointer.response = resp } @@ -156,36 +115,12 @@ func (c *Client) setPointers(resp *http.Response) { } } -// initRequest will apply all the client setting, common option and request option -func (c *Client) newRequest(method, path string, opts []BeegoHttpRequestOption) (*BeegoHTTPRequest, error) { - var req *BeegoHTTPRequest - switch method { - case http.MethodGet: - req = Get(c.Endpoint + path) - case http.MethodPost: - req = Post(c.Endpoint + path) - case http.MethodPut: - req = Put(c.Endpoint + path) - case http.MethodDelete: - req = Delete(c.Endpoint + path) - case http.MethodHead: - req = Head(c.Endpoint + path) - } - - req = req.Setting(*c.Setting) - for _, o := range c.CommonOpts { - err := o(req) - if err != nil { - return nil, err - } - } +func (c *Client) customReq(req *BeegoHTTPRequest, opts []BeegoHttpRequestOption) { + req.Setting(c.Setting) + opts = append(c.CommonOpts, opts...) for _, o := range opts { - err := o(req) - if err != nil { - return nil, err - } + o(req) } - return req, nil } // handleResponse try to parse body to meaningful value @@ -196,69 +131,20 @@ func (c *Client) handleResponse(value interface{}, req *BeegoHTTPRequest) error return err } c.setPointers(resp) - - if value == nil { - return nil - } - - // handle basic type - switch v := value.(type) { - case **string: - s, err := req.String() - if err != nil { - return nil - } - *v = &s - return nil - case **[]byte: - bs, err := req.Bytes() - if err != nil { - return nil - } - *v = &bs - return nil - } - - // try to parse it as content type - switch strings.Split(resp.Header.Get("Content-Type"), ";")[0] { - case "application/json": - return req.ToJSON(value) - case "text/xml", "application/xml": - return req.ToXML(value) - case "text/yaml", "application/x-yaml": - return req.ToYAML(value) - } - - // try to parse it anyway - if err := req.ToJSON(value); err == nil { - return nil - } - if err := req.ToYAML(value); err == nil { - return nil - } - if err := req.ToXML(value); err == nil { - return nil - } - - // TODO add new error type about can't parse body - return berror.Error(UnsupportedBodyType, "unsupported body data") + return req.ResponseForValue(value) } // Get Send a GET request and try to give its result value func (c *Client) Get(value interface{}, path string, opts ...BeegoHttpRequestOption) error { - req, err := c.newRequest(http.MethodGet, path, opts) - if err != nil { - return err - } + req := Get(c.Endpoint + path) + c.customReq(req, opts) return c.handleResponse(value, req) } // Post Send a POST request and try to give its result value func (c *Client) Post(value interface{}, path string, body interface{}, opts ...BeegoHttpRequestOption) error { - req, err := c.newRequest(http.MethodPost, path, opts) - if err != nil { - return err - } + req := Post(c.Endpoint + path) + c.customReq(req, opts) if body != nil { req = req.Body(body) } @@ -267,10 +153,8 @@ func (c *Client) Post(value interface{}, path string, body interface{}, opts ... // Put Send a Put request and try to give its result value func (c *Client) Put(value interface{}, path string, body interface{}, opts ...BeegoHttpRequestOption) error { - req, err := c.newRequest(http.MethodPut, path, opts) - if err != nil { - return err - } + req := Put(c.Endpoint + path) + c.customReq(req, opts) if body != nil { req = req.Body(body) } @@ -279,18 +163,14 @@ func (c *Client) Put(value interface{}, path string, body interface{}, opts ...B // Delete Send a Delete request and try to give its result value func (c *Client) Delete(value interface{}, path string, opts ...BeegoHttpRequestOption) error { - req, err := c.newRequest(http.MethodDelete, path, opts) - if err != nil { - return err - } + req := Delete(c.Endpoint + path) + c.customReq(req, opts) return c.handleResponse(value, req) } // Head Send a Head request and try to give its result value func (c *Client) Head(value interface{}, path string, opts ...BeegoHttpRequestOption) error { - req, err := c.newRequest(http.MethodHead, path, opts) - if err != nil { - return err - } + req := Head(c.Endpoint + path) + c.customReq(req, opts) return c.handleResponse(value, req) } diff --git a/client/httplib/httplib.go b/client/httplib/httplib.go index cef2294c..317c462c 100644 --- a/client/httplib/httplib.go +++ b/client/httplib/httplib.go @@ -56,6 +56,7 @@ import ( ) const contentTypeKey = "Content-Type" + // it will be the last filter and execute request.Do var doRequestFilter = func(ctx context.Context, req *BeegoHTTPRequest) (*http.Response, error) { return req.doRequest(ctx) @@ -660,6 +661,64 @@ func (b *BeegoHTTPRequest) Response() (*http.Response, error) { return b.getResponse() } +// ResponseForValue attempts to resolve the response body to value using an existing method. +// Calls Response inner. +// If value type is **string or **[]byte, the func directly passes response body into the pointer. +// Else if response header contain Content-Type, func will call ToJSON\ToXML\ToYAML. +// Finally it will try to parse body as json\yaml\xml, If all attempts fail, an error will be returned +func (b *BeegoHTTPRequest) ResponseForValue(value interface{}) error { + if value == nil { + return nil + } + // handle basic type + switch v := value.(type) { + case **string: + s, err := b.String() + if err != nil { + return nil + } + *v = &s + return nil + case **[]byte: + bs, err := b.Bytes() + if err != nil { + return nil + } + *v = &bs + return nil + } + + resp, err := b.Response() + if err != nil { + return err + } + contentType := strings.Split(resp.Header.Get(contentTypeKey), ";")[0] + + // try to parse it as content type + switch contentType { + case "application/json": + return b.ToJSON(value) + case "text/xml", "application/xml": + return b.ToXML(value) + case "text/yaml", "application/x-yaml", "application/x+yaml": + return b.ToYAML(value) + } + + // try to parse it anyway + if err := b.ToJSON(value); err == nil { + return nil + } + if err := b.ToYAML(value); err == nil { + return nil + } + if err := b.ToXML(value); err == nil { + return nil + } + + // TODO add new error type about can't parse body + return berror.Error(UnsupportedBodyType, "unsupported body data") +} + // TimeoutDialer returns functions of connection dialer with timeout settings for http.Transport Dial field. // Deprecated // we will move this at the end of 2021 diff --git a/client/httplib/httplib_test.go b/client/httplib/httplib_test.go index 9133ad5f..e7939a4e 100644 --- a/client/httplib/httplib_test.go +++ b/client/httplib/httplib_test.go @@ -433,3 +433,7 @@ func TestBeegoHTTPRequest_XMLBody(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, req.req.GetBody) } + +// TODO +func TestBeegoHTTPRequest_ResponseForValue(t *testing.T) { +} diff --git a/client/httplib/setting.go b/client/httplib/setting.go index 542c39be..2d7a0eed 100644 --- a/client/httplib/setting.go +++ b/client/httplib/setting.go @@ -55,7 +55,7 @@ func SetDefaultSetting(setting BeegoHTTPSettings) { defaultSetting = setting } -// SetDefaultSetting return current default setting +// GetDefaultSetting return current default setting func GetDefaultSetting() BeegoHTTPSettings { return defaultSetting }