diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fa7111..87a34e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # developing +- Add http client and option func. [4455](https://github.com/beego/beego/issues/4455) - Add: Convenient way to generate mock object [4620](https://github.com/beego/beego/issues/4620) - Infra: use dependabot to update dependencies. [4623](https://github.com/beego/beego/pull/4623) - Lint: use golangci-lint. [4619](https://github.com/beego/beego/pull/4619) diff --git a/client/httplib/README.md b/client/httplib/README.md index 1d22f341..a5723c6d 100644 --- a/client/httplib/README.md +++ b/client/httplib/README.md @@ -9,7 +9,7 @@ httplib is an libs help you to curl remote url. you can use Get to crawl data. import "github.com/beego/beego/v2/client/httplib" - + str, err := httplib.Get("http://beego.me/").String() if err != nil { // error @@ -39,7 +39,7 @@ Example: // GET httplib.Get("http://beego.me/").SetTimeout(100 * time.Second, 30 * time.Second) - + // POST httplib.Post("http://beego.me/").SetTimeout(100 * time.Second, 30 * time.Second) diff --git a/client/httplib/client_option.go b/client/httplib/client_option.go new file mode 100644 index 00000000..f970e67d --- /dev/null +++ b/client/httplib/client_option.go @@ -0,0 +1,155 @@ +// Copyright 2020 beego +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httplib + +import ( + "crypto/tls" + "net/http" + "net/url" + "time" +) + +type ( + ClientOption func(client *Client) + BeegoHTTPRequestOption func(request *BeegoHTTPRequest) +) + +// WithEnableCookie will enable cookie in all subsequent request +func WithEnableCookie(enable bool) ClientOption { + return func(client *Client) { + client.Setting.EnableCookie = enable + } +} + +// WithEnableCookie will adds UA in all subsequent request +func WithUserAgent(userAgent string) ClientOption { + return func(client *Client) { + client.Setting.UserAgent = userAgent + } +} + +// WithTLSClientConfig will adds tls config in all subsequent request +func WithTLSClientConfig(config *tls.Config) ClientOption { + return func(client *Client) { + client.Setting.TLSClientConfig = config + } +} + +// WithTransport will set transport field in all subsequent request +func WithTransport(transport http.RoundTripper) ClientOption { + return func(client *Client) { + client.Setting.Transport = transport + } +} + +// WithProxy will set http proxy field in all subsequent request +func WithProxy(proxy func(*http.Request) (*url.URL, error)) ClientOption { + return func(client *Client) { + client.Setting.Proxy = proxy + } +} + +// 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) { + client.Setting.CheckRedirect = redirect + } +} + +// WithHTTPSetting can replace beegoHTTPSeting +func WithHTTPSetting(setting BeegoHTTPSettings) ClientOption { + return func(client *Client) { + client.Setting = setting + } +} + +// WithEnableGzip will enable gzip in all subsequent request +func WithEnableGzip(enable bool) ClientOption { + return func(client *Client) { + client.Setting.Gzip = enable + } +} + +// BeegoHttpRequestOption + +// WithTimeout sets connect time out and read-write time out for BeegoRequest. +func WithTimeout(connectTimeout, readWriteTimeout time.Duration) BeegoHTTPRequestOption { + return func(request *BeegoHTTPRequest) { + request.SetTimeout(connectTimeout, readWriteTimeout) + } +} + +// WithHeader adds header item string in request. +func WithHeader(key, value string) BeegoHTTPRequestOption { + return func(request *BeegoHTTPRequest) { + request.Header(key, value) + } +} + +// WithCookie adds a cookie to the request. +func WithCookie(cookie *http.Cookie) BeegoHTTPRequestOption { + return func(request *BeegoHTTPRequest) { + request.Header("Cookie", cookie.String()) + } +} + +// Withtokenfactory adds a custom function to set Authorization +func WithTokenFactory(tokenFactory func() string) BeegoHTTPRequestOption { + return func(request *BeegoHTTPRequest) { + t := tokenFactory() + + request.Header("Authorization", t) + } +} + +// WithBasicAuth adds a custom function to set basic auth +func WithBasicAuth(basicAuth func() (string, string)) BeegoHTTPRequestOption { + return func(request *BeegoHTTPRequest) { + username, password := basicAuth() + request.SetBasicAuth(username, password) + } +} + +// WithFilters will use the filter as the invocation filters +func WithFilters(fcs ...FilterChain) BeegoHTTPRequestOption { + return func(request *BeegoHTTPRequest) { + request.SetFilters(fcs...) + } +} + +// WithContentType adds ContentType in header +func WithContentType(contentType string) BeegoHTTPRequestOption { + 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) { + request.Param(key, value) + } +} + +// WithRetry set retry times and delay for the request +// default is 0 (never retry) +// -1 retry indefinitely (forever) +// Other numbers specify the exact retry amount +func WithRetry(times int, delay time.Duration) BeegoHTTPRequestOption { + return func(request *BeegoHTTPRequest) { + request.Retries(times) + request.RetryDelay(delay) + } +} diff --git a/client/httplib/client_option_test.go b/client/httplib/client_option_test.go new file mode 100644 index 00000000..79e5103f --- /dev/null +++ b/client/httplib/client_option_test.go @@ -0,0 +1,261 @@ +// Copyright 2020 beego +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httplib + +import ( + "errors" + "net" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type respCarrier struct { + bytes []byte +} + +func (r *respCarrier) SetBytes(bytes []byte) { + r.bytes = bytes +} + +func (r *respCarrier) String() string { + return string(r.bytes) +} + +func TestOption_WithEnableCookie(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org/", + WithEnableCookie(true)) + if err != nil { + t.Fatal(err) + } + + v := "smallfish" + resp := &respCarrier{} + err = client.Get(resp, "/cookies/set?k1="+v) + if err != nil { + t.Fatal(err) + } + t.Log(resp.String()) + + err = client.Get(resp, "/cookies") + if err != nil { + t.Fatal(err) + } + t.Log(resp.String()) + + n := strings.Index(resp.String(), v) + if n == -1 { + t.Fatal(v + " not found in cookie") + } +} + +func TestOption_WithUserAgent(t *testing.T) { + v := "beego" + client, err := NewClient("test", "http://httpbin.org/", + WithUserAgent(v)) + if err != nil { + t.Fatal(err) + } + + resp := &respCarrier{} + err = client.Get(resp, "/headers") + if err != nil { + t.Fatal(err) + } + t.Log(resp.String()) + + n := strings.Index(resp.String(), v) + if n == -1 { + t.Fatal(v + " not found in user-agent") + } +} + +func TestOption_WithCheckRedirect(t *testing.T) { + client, err := NewClient("test", "https://goolnk.com/33BD2j", + WithCheckRedirect(func(redirectReq *http.Request, redirectVia []*http.Request) error { + return errors.New("Redirect triggered") + })) + if err != nil { + t.Fatal(err) + } + err = client.Get(nil, "") + assert.NotNil(t, err) +} + +func TestOption_WithHTTPSetting(t *testing.T) { + v := "beego" + var setting BeegoHTTPSettings + setting.EnableCookie = true + setting.UserAgent = v + setting.Transport = &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 50, + IdleConnTimeout: 90 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } + setting.ReadWriteTimeout = 5 * time.Second + + client, err := NewClient("test", "http://httpbin.org/", + WithHTTPSetting(setting)) + if err != nil { + t.Fatal(err) + } + + resp := &respCarrier{} + err = client.Get(resp, "/get") + if err != nil { + t.Fatal(err) + } + t.Log(resp.String()) + + n := strings.Index(resp.String(), v) + if n == -1 { + t.Fatal(v + " not found in user-agent") + } +} + +func TestOption_WithHeader(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org/") + if err != nil { + t.Fatal(err) + } + client.CommonOpts = append(client.CommonOpts, WithHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.57 Safari/537.36")) + + resp := &respCarrier{} + err = client.Get(resp, "/headers") + if err != nil { + t.Fatal(err) + } + t.Log(resp.String()) + + n := strings.Index(resp.String(), "Mozilla/5.0") + if n == -1 { + t.Fatal("Mozilla/5.0 not found in user-agent") + } +} + +func TestOption_WithTokenFactory(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org/") + if err != nil { + t.Fatal(err) + } + client.CommonOpts = append(client.CommonOpts, + WithTokenFactory(func() string { + return "testauth" + })) + + resp := &respCarrier{} + err = client.Get(resp, "/headers") + if err != nil { + t.Fatal(err) + } + t.Log(resp.String()) + + n := strings.Index(resp.String(), "testauth") + if n == -1 { + t.Fatal("Auth is not set in request") + } +} + +func TestOption_WithBasicAuth(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org/") + if err != nil { + t.Fatal(err) + } + + resp := &respCarrier{} + err = client.Get(resp, "/basic-auth/user/passwd", + WithBasicAuth(func() (string, string) { + return "user", "passwd" + })) + if err != nil { + t.Fatal(err) + } + t.Log(resp.String()) + n := strings.Index(resp.String(), "authenticated") + if n == -1 { + t.Fatal("authenticated not found in response") + } +} + +func TestOption_WithContentType(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org/") + if err != nil { + t.Fatal(err) + } + + v := "application/json" + resp := &respCarrier{} + err = client.Get(resp, "/headers", WithContentType(v)) + if err != nil { + t.Fatal(err) + } + t.Log(resp.String()) + + n := strings.Index(resp.String(), v) + if n == -1 { + t.Fatal(v + " not found in header") + } +} + +func TestOption_WithParam(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org/") + if err != nil { + t.Fatal(err) + } + + v := "smallfish" + resp := &respCarrier{} + err = client.Get(resp, "/get", WithParam("username", v)) + if err != nil { + t.Fatal(err) + } + t.Log(resp.String()) + + n := strings.Index(resp.String(), v) + if n == -1 { + t.Fatal(v + " not found in header") + } +} + +func TestOption_WithRetry(t *testing.T) { + client, err := NewClient("test", "https://goolnk.com/33BD2j", + WithCheckRedirect(func(redirectReq *http.Request, redirectVia []*http.Request) error { + return errors.New("Redirect triggered") + })) + if err != nil { + t.Fatal(err) + } + + retryAmount := 1 + retryDelay := 800 * time.Millisecond + startTime := time.Now().UnixNano() / int64(time.Millisecond) + + _ = client.Get(nil, "", WithRetry(retryAmount, retryDelay)) + + endTime := time.Now().UnixNano() / int64(time.Millisecond) + elapsedTime := endTime - startTime + delayedTime := int64(retryAmount) * retryDelay.Milliseconds() + if elapsedTime < delayedTime { + t.Errorf("Not enough retries. Took %dms. Delay was meant to take %dms", elapsedTime, delayedTime) + } +} diff --git a/client/httplib/error_code.go b/client/httplib/error_code.go index bd349a34..177419ad 100644 --- a/client/httplib/error_code.go +++ b/client/httplib/error_code.go @@ -124,3 +124,11 @@ Make sure that: 1. You pass valid structure pointer to the function; 2. The body is valid YAML document `) + +var UnmarshalResponseToObjectFailed = berror.DefineCode(5001011, moduleName, + "UnmarshalResponseToObjectFailed", ` +Beego trying to unmarshal response's body to structure but failed. +There are several cases that cause this error: +1. You pass valid structure pointer to the function; +2. The body is valid json, Yaml or XML document +`) diff --git a/client/httplib/httpclient.go b/client/httplib/httpclient.go new file mode 100644 index 00000000..c2a61fcf --- /dev/null +++ b/client/httplib/httpclient.go @@ -0,0 +1,174 @@ +// Copyright 2020 beego +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httplib + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" +) + +// Client provides an HTTP client supporting chain call +type Client struct { + Name string + Endpoint string + CommonOpts []BeegoHTTPRequestOption + + Setting BeegoHTTPSettings +} + +// HTTPResponseCarrier If value implement HTTPResponseCarrier. http.Response will pass to SetHTTPResponse +type HTTPResponseCarrier interface { + SetHTTPResponse(resp *http.Response) +} + +// HTTPBodyCarrier If value implement HTTPBodyCarrier. http.Response.Body will pass to SetReader +type HTTPBodyCarrier interface { + SetReader(r io.ReadCloser) +} + +// HTTPBytesCarrier If value implement HTTPBytesCarrier. +// All the byte in http.Response.Body will pass to SetBytes +type HTTPBytesCarrier interface { + SetBytes(bytes []byte) +} + +// HTTPStatusCarrier If value implement HTTPStatusCarrier. http.Response.StatusCode will pass to SetStatusCode +type HTTPStatusCarrier interface { + SetStatusCode(status int) +} + +// HttpHeaderCarrier If value implement HttpHeaderCarrier. http.Response.Header will pass to SetHeader +type HTTPHeadersCarrier interface { + SetHeader(header map[string][]string) +} + +// NewClient return a new http client +func NewClient(name string, endpoint string, opts ...ClientOption) (*Client, error) { + res := &Client{ + Name: name, + Endpoint: endpoint, + } + setting := GetDefaultSetting() + res.Setting = setting + for _, o := range opts { + o(res) + } + return res, nil +} + +func (c *Client) customReq(req *BeegoHTTPRequest, opts []BeegoHTTPRequestOption) { + req.Setting(c.Setting) + opts = append(c.CommonOpts, opts...) + for _, o := range opts { + o(req) + } +} + +// handleResponse try to parse body to meaningful value +func (c *Client) handleResponse(value interface{}, req *BeegoHTTPRequest) error { + // make sure req.resp is not nil + _, err := req.Bytes() + if err != nil { + return err + } + + err = c.handleCarrier(value, req) + if err != nil { + return err + } + + return req.ToValue(value) +} + +// handleCarrier set http data to value +func (c *Client) handleCarrier(value interface{}, req *BeegoHTTPRequest) error { + if value == nil { + return nil + } + + if carrier, ok := value.(HTTPResponseCarrier); ok { + b, err := req.Bytes() + if err != nil { + return err + } + req.resp.Body = ioutil.NopCloser(bytes.NewReader(b)) + carrier.SetHTTPResponse(req.resp) + } + if carrier, ok := value.(HTTPBodyCarrier); ok { + b, err := req.Bytes() + if err != nil { + return err + } + reader := ioutil.NopCloser(bytes.NewReader(b)) + carrier.SetReader(reader) + } + if carrier, ok := value.(HTTPBytesCarrier); ok { + b, err := req.Bytes() + if err != nil { + return err + } + carrier.SetBytes(b) + } + if carrier, ok := value.(HTTPStatusCarrier); ok { + carrier.SetStatusCode(req.resp.StatusCode) + } + if carrier, ok := value.(HTTPHeadersCarrier); ok { + carrier.SetHeader(req.resp.Header) + } + return nil +} + +// Get Send a GET request and try to give its result value +func (c *Client) Get(value interface{}, path string, opts ...BeegoHTTPRequestOption) error { + 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 := Post(c.Endpoint + path) + c.customReq(req, opts) + if body != nil { + req = req.Body(body) + } + return c.handleResponse(value, req) +} + +// 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 := Put(c.Endpoint + path) + c.customReq(req, opts) + if body != nil { + req = req.Body(body) + } + return c.handleResponse(value, req) +} + +// Delete Send a Delete request and try to give its result value +func (c *Client) Delete(value interface{}, path string, opts ...BeegoHTTPRequestOption) error { + 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 := Head(c.Endpoint + path) + c.customReq(req, opts) + return c.handleResponse(value, req) +} diff --git a/client/httplib/httpclient_test.go b/client/httplib/httpclient_test.go new file mode 100644 index 00000000..6bb00258 --- /dev/null +++ b/client/httplib/httpclient_test.go @@ -0,0 +1,220 @@ +// Copyright 2020 beego +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httplib + +import ( + "encoding/xml" + "io" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewClient(t *testing.T) { + client, err := NewClient("test1", "http://beego.me", WithEnableCookie(true)) + assert.NoError(t, err) + assert.NotNil(t, client) + assert.Equal(t, true, client.Setting.EnableCookie) +} + +type slideShowResponse struct { + Resp *http.Response + bytes []byte + StatusCode int + Body io.ReadCloser + Header map[string][]string + + Slideshow slideshow `json:"slideshow" yaml:"slideshow"` +} + +func (r *slideShowResponse) SetHTTPResponse(resp *http.Response) { + r.Resp = resp +} + +func (r *slideShowResponse) SetBytes(bytes []byte) { + r.bytes = bytes +} + +func (r *slideShowResponse) SetReader(reader io.ReadCloser) { + r.Body = reader +} + +func (r *slideShowResponse) SetStatusCode(status int) { + r.StatusCode = status +} + +func (r *slideShowResponse) SetHeader(header map[string][]string) { + r.Header = header +} + +func (r *slideShowResponse) String() string { + return string(r.bytes) +} + +type slideshow struct { + XMLName xml.Name `xml:"slideshow"` + + Title string `json:"title" yaml:"title" xml:"title,attr"` + Author string `json:"author" yaml:"author" xml:"author,attr"` + Date string `json:"date" yaml:"date" xml:"date,attr"` + Slides []slide `json:"slides" yaml:"slides" xml:"slide"` +} + +type slide struct { + XMLName xml.Name `xml:"slide"` + + Title string `json:"title" yaml:"title" xml:"title"` +} + +func TestClient_handleCarrier(t *testing.T) { + v := "beego" + client, err := NewClient("test", "http://httpbin.org/", + WithUserAgent(v)) + if err != nil { + t.Fatal(err) + } + + s := &slideShowResponse{} + err = client.Get(s, "/json") + if err != nil { + t.Fatal(err) + } + defer s.Body.Close() + + assert.NotNil(t, s.Resp) + assert.NotNil(t, s.Body) + assert.Equal(t, "429", s.Header["Content-Length"][0]) + assert.Equal(t, 200, s.StatusCode) + + b, err := ioutil.ReadAll(s.Body) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, 429, len(b)) + assert.Equal(t, s.String(), string(b)) +} + +func TestClient_Get(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org/") + if err != nil { + t.Fatal(err) + } + + // json + var s *slideShowResponse + err = client.Get(&s, "/json") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "Sample Slide Show", s.Slideshow.Title) + assert.Equal(t, 2, len(s.Slideshow.Slides)) + assert.Equal(t, "Overview", s.Slideshow.Slides[1].Title) + + // xml + var ssp *slideshow + err = client.Get(&ssp, "/base64/PD94bWwgPz48c2xpZGVzaG93CnRpdGxlPSJTYW1wbGUgU2xpZGUgU2hvdyIKZGF0ZT0iRGF0ZSBvZiBwdWJsaWNhdGlvbiIKYXV0aG9yPSJZb3VycyBUcnVseSI+PHNsaWRlIHR5cGU9ImFsbCI+PHRpdGxlPldha2UgdXAgdG8gV29uZGVyV2lkZ2V0cyE8L3RpdGxlPjwvc2xpZGU+PHNsaWRlIHR5cGU9ImFsbCI+PHRpdGxlPk92ZXJ2aWV3PC90aXRsZT48aXRlbT5XaHkgPGVtPldvbmRlcldpZGdldHM8L2VtPiBhcmUgZ3JlYXQ8L2l0ZW0+PGl0ZW0vPjxpdGVtPldobyA8ZW0+YnV5czwvZW0+IFdvbmRlcldpZGdldHM8L2l0ZW0+PC9zbGlkZT48L3NsaWRlc2hvdz4=") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "Sample Slide Show", ssp.Title) + assert.Equal(t, 2, len(ssp.Slides)) + assert.Equal(t, "Overview", ssp.Slides[1].Title) + + // yaml + s = nil + err = client.Get(&s, "/base64/c2xpZGVzaG93OgogIGF1dGhvcjogWW91cnMgVHJ1bHkKICBkYXRlOiBkYXRlIG9mIHB1YmxpY2F0aW9uCiAgc2xpZGVzOgogIC0gdGl0bGU6IFdha2UgdXAgdG8gV29uZGVyV2lkZ2V0cyEKICAgIHR5cGU6IGFsbAogIC0gaXRlbXM6CiAgICAtIFdoeSA8ZW0+V29uZGVyV2lkZ2V0czwvZW0+IGFyZSBncmVhdAogICAgLSBXaG8gPGVtPmJ1eXM8L2VtPiBXb25kZXJXaWRnZXRzCiAgICB0aXRsZTogT3ZlcnZpZXcKICAgIHR5cGU6IGFsbAogIHRpdGxlOiBTYW1wbGUgU2xpZGUgU2hvdw==") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "Sample Slide Show", s.Slideshow.Title) + assert.Equal(t, 2, len(s.Slideshow.Slides)) + assert.Equal(t, "Overview", s.Slideshow.Slides[1].Title) +} + +func TestClient_Post(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org") + if err != nil { + t.Fatal(err) + } + + resp := &slideShowResponse{} + err = client.Get(resp, "/json") + if err != nil { + t.Fatal(err) + } + + jsonStr := resp.String() + err = client.Post(resp, "/post", jsonStr) + if err != nil { + t.Fatal(err) + } + assert.NotNil(t, resp) + assert.Equal(t, http.MethodPost, resp.Resp.Request.Method) +} + +func TestClient_Put(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org") + if err != nil { + t.Fatal(err) + } + + resp := &slideShowResponse{} + err = client.Get(resp, "/json") + if err != nil { + t.Fatal(err) + } + + jsonStr := resp.String() + err = client.Put(resp, "/put", jsonStr) + if err != nil { + t.Fatal(err) + } + assert.NotNil(t, resp) + assert.Equal(t, http.MethodPut, resp.Resp.Request.Method) +} + +func TestClient_Delete(t *testing.T) { + client, err := NewClient("test", "http://httpbin.org") + if err != nil { + t.Fatal(err) + } + + resp := &slideShowResponse{} + err = client.Delete(resp, "/delete") + if err != nil { + t.Fatal(err) + } + defer resp.Resp.Body.Close() + assert.NotNil(t, resp) + assert.Equal(t, http.MethodDelete, resp.Resp.Request.Method) +} + +func TestClient_Head(t *testing.T) { + client, err := NewClient("test", "http://beego.me") + if err != nil { + t.Fatal(err) + } + + resp := &slideShowResponse{} + err = client.Head(resp, "") + if err != nil { + t.Fatal(err) + } + defer resp.Resp.Body.Close() + assert.NotNil(t, resp) + assert.Equal(t, http.MethodHead, resp.Resp.Request.Method) +} diff --git a/client/httplib/httplib.go b/client/httplib/httplib.go index 434d74c1..b102f687 100644 --- a/client/httplib/httplib.go +++ b/client/httplib/httplib.go @@ -124,7 +124,6 @@ type BeegoHTTPRequest struct { setting BeegoHTTPSettings resp *http.Response body []byte - dump []byte } // GetRequest returns the request object @@ -199,7 +198,7 @@ func (b *BeegoHTTPRequest) SetHost(host string) *BeegoHTTPRequest { // SetProtocolVersion sets the protocol version for incoming requests. // Client requests always use HTTP/1.1 func (b *BeegoHTTPRequest) SetProtocolVersion(vers string) *BeegoHTTPRequest { - if len(vers) == 0 { + if vers == "" { vers = "HTTP/1.1" } @@ -511,18 +510,16 @@ func (b *BeegoHTTPRequest) buildTrans() http.RoundTripper { DialContext: TimeoutDialerCtx(b.setting.ConnectTimeout, b.setting.ReadWriteTimeout), MaxIdleConnsPerHost: 100, } - } else { + } else if t, ok := trans.(*http.Transport); ok { // if b.transport is *http.Transport then set the settings. - if t, ok := trans.(*http.Transport); ok { - if t.TLSClientConfig == nil { - t.TLSClientConfig = b.setting.TLSClientConfig - } - if t.Proxy == nil { - t.Proxy = b.setting.Proxy - } - if t.DialContext == nil { - t.DialContext = TimeoutDialerCtx(b.setting.ConnectTimeout, b.setting.ReadWriteTimeout) - } + if t.TLSClientConfig == nil { + t.TLSClientConfig = b.setting.TLSClientConfig + } + if t.Proxy == nil { + t.Proxy = b.setting.Proxy + } + if t.DialContext == nil { + t.DialContext = TimeoutDialerCtx(b.setting.ConnectTimeout, b.setting.ReadWriteTimeout) } } return trans @@ -656,6 +653,40 @@ func (b *BeegoHTTPRequest) ToYAML(v interface{}) error { UnmarshalYAMLResponseToObjectFailed, "unmarshal yaml body to object failed.") } +// ToValue attempts to resolve the response body to value using an existing method. +// Calls Response inner. +// If response header contain Content-Type, func will call ToJSON\ToXML\ToYAML. +// Else it will try to parse body as json\yaml\xml, If all attempts fail, an error will be returned +func (b *BeegoHTTPRequest) ToValue(value interface{}) error { + if value == nil { + return nil + } + + contentType := strings.Split(b.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 + } + + return berror.Error(UnmarshalResponseToObjectFailed, "unmarshal body to object failed.") +} + // Response executes request client gets response manually. func (b *BeegoHTTPRequest) Response() (*http.Response, error) { return b.getResponse() diff --git a/client/httplib/httplib_test.go b/client/httplib/httplib_test.go index be702fb6..491b1b9f 100644 --- a/client/httplib/httplib_test.go +++ b/client/httplib/httplib_test.go @@ -433,3 +433,7 @@ func TestBeegoHTTPRequestXMLBody(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/mock/mock_condition.go b/client/httplib/mock/mock_condition.go index 53d3d703..912699ff 100644 --- a/client/httplib/mock/mock_condition.go +++ b/client/httplib/mock/mock_condition.go @@ -57,7 +57,7 @@ func NewSimpleCondition(path string, opts ...simpleConditionOption) *SimpleCondi } func (sc *SimpleCondition) Match(ctx context.Context, req *httplib.BeegoHTTPRequest) bool { - res := true + var res bool if len(sc.path) > 0 { res = sc.matchPath(ctx, req) } else if len(sc.pathReg) > 0 { diff --git a/client/httplib/setting.go b/client/httplib/setting.go index df8eff4b..2d7a0eed 100644 --- a/client/httplib/setting.go +++ b/client/httplib/setting.go @@ -55,6 +55,11 @@ func SetDefaultSetting(setting BeegoHTTPSettings) { defaultSetting = setting } +// GetDefaultSetting return current default setting +func GetDefaultSetting() BeegoHTTPSettings { + return defaultSetting +} + var defaultSetting = BeegoHTTPSettings{ UserAgent: "beegoServer", ConnectTimeout: 60 * time.Second, diff --git a/go.mod b/go.mod index 0305be1e..99b24947 100644 --- a/go.mod +++ b/go.mod @@ -37,4 +37,5 @@ require ( golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a google.golang.org/grpc v1.37.1 gopkg.in/yaml.v2 v2.4.0 + mvdan.cc/gofumpt v0.1.1 // indirect ) diff --git a/go.sum b/go.sum index c144e80d..91616ec7 100644 --- a/go.sum +++ b/go.sum @@ -333,6 +333,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= @@ -366,6 +367,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112 h1:NBrpnvz0pDPf3+HXZ1C9GcJd1DTpWDLcLWZhNq6uP7o= @@ -427,6 +429,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -512,6 +516,7 @@ golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -589,6 +594,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +mvdan.cc/gofumpt v0.1.1 h1:bi/1aS/5W00E2ny5q65w9SnKpWEF/UIOqDYBILpo9rA= +mvdan.cc/gofumpt v0.1.1/go.mod h1:yXG1r1WqZVKWbVRtBWKWX9+CxGYfA51nSomhM0woR48= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=