From 0055921ec9c615576e51779ab61ad46c103ff782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=82=85=E9=B9=8F?= Date: Sun, 24 Dec 2023 13:16:14 +0000 Subject: [PATCH] feat: add custom json/xml marsher --- config.go | 74 ++++++++++++++++++++++++++++++-------------- examples/api/main.go | 2 +- multipart_file.go | 6 +--- options.go | 6 ++-- response.go | 6 ++-- surf.go | 18 ++++++----- utils.go | 17 ++++++++++ utils_test.go | 24 ++++++++++++++ 8 files changed, 109 insertions(+), 44 deletions(-) create mode 100644 utils_test.go diff --git a/config.go b/config.go index 9eb8964..8dfdc6f 100644 --- a/config.go +++ b/config.go @@ -19,7 +19,9 @@ type ( // ResponseInterceptor defines a function signature for response interceptors. ResponseInterceptor func(resp *Response) error - RequestInterceptorChain []RequestInterceptor + // RequestInterceptorChain alias for RequestInterceptors + RequestInterceptorChain []RequestInterceptor + // ResponseInterceptorChain alias for ResponseInterceptors ResponseInterceptorChain []ResponseInterceptor // QuerySerializer is responsible for encoding URL query parameters. @@ -30,7 +32,7 @@ type ( // Config holds the configuration for Surf. Config struct { BaseURL string - Headers http.Header + Header http.Header Timeout time.Duration Cookies []*http.Cookie CookieJar *http.CookieJar @@ -50,13 +52,18 @@ type ( MaxRedirects int Client *http.Client + + JSONMarshal func(v interface{}) ([]byte, error) + JSONUnmarshal func(data []byte, v interface{}) error + XMLMarshal func(v interface{}) ([]byte, error) + XMLUnmarshal func(data []byte, v interface{}) error } // RequestConfig holds the configuration for a specific HTTP request. RequestConfig struct { BaseURL string Url string - Headers http.Header + Header http.Header Method string Cookies []*http.Cookie @@ -85,6 +92,11 @@ type ( Request *http.Request clientTrace *clientTrace + + JSONMarshal func(v interface{}) ([]byte, error) + JSONUnmarshal func(data []byte, v interface{}) error + XMLMarshal func(v interface{}) ([]byte, error) + XMLUnmarshal func(data []byte, v interface{}) error } ) @@ -156,10 +168,10 @@ func (rc *RequestConfig) SetParam(key, value string) *RequestConfig { // SetHeader sets a header in the request configuration. func (rc *RequestConfig) SetHeader(key, value string) *RequestConfig { - if rc.Headers == nil { - rc.Headers = make(http.Header) + if rc.Header == nil { + rc.Header = make(http.Header) } - rc.Headers.Set(key, value) + rc.Header.Set(key, value) return rc } @@ -236,28 +248,32 @@ func (rc *RequestConfig) getRequestBody() (r io.Reader, err error) { case string: return bytes.NewReader([]byte(data)), err default: - contentType := rc.Headers.Get(headerContentType) - if contentType != "" && regXmlHeader.MatchString(contentType) { - xmlData, xmlErr := xml.Marshal(data) - if xmlErr != nil { - return nil, xmlErr + contentType := rc.Header.Get(headerContentType) + if contentType != "" { + if regXmlHeader.MatchString(contentType) { + xmlData, xmlErr := rc.XMLMarshal(data) + if xmlErr != nil { + return nil, xmlErr + } + return bytes.NewReader(xmlData), nil } - return bytes.NewReader(xmlData), nil - } - if contentType != "" && regJsonHeader.MatchString(contentType) { - jsonData, jsonErr := json.Marshal(data) - if jsonErr != nil { - return nil, jsonErr + + if regJsonHeader.MatchString(contentType) { + jsonData, jsonErr := rc.JSONMarshal(data) + if jsonErr != nil { + return nil, jsonErr + } + return bytes.NewReader(jsonData), nil } - return bytes.NewReader(jsonData), nil } + return nil, ErrRequestDataTypeInvalid } } // setContentTypeHeader sets the Content-Type header based on the request body type. func (rc *RequestConfig) setContentTypeHeader() { - if rc.Headers.Get(headerContentType) != "" { + if rc.Header.Get(headerContentType) != "" { return } @@ -284,17 +300,13 @@ func (rc *RequestConfig) mergeConfig(config *Config) *RequestConfig { } if rc.Client == nil { - rc.Client = config.Client + rc.Client = defaultValue(config.Client, http.DefaultClient) } if rc.Timeout == 0 { rc.Timeout = config.Timeout } - if rc.Client == nil { - rc.Client = http.DefaultClient - } - if config.CookieJar != nil { rc.Client.Jar = *config.CookieJar } @@ -337,6 +349,20 @@ func (rc *RequestConfig) mergeConfig(config *Config) *RequestConfig { } } + if rc.JSONMarshal == nil { + rc.JSONMarshal = defaultValue(config.JSONMarshal, json.Marshal) + } + if rc.JSONUnmarshal == nil { + rc.JSONUnmarshal = defaultValue(config.JSONUnmarshal, json.Unmarshal) + } + + if rc.XMLMarshal == nil { + rc.XMLMarshal = defaultValue(config.XMLMarshal, xml.Marshal) + } + if rc.XMLUnmarshal == nil { + rc.XMLUnmarshal = defaultValue(config.XMLUnmarshal, xml.Unmarshal) + } + // Enable http trace for Performance rc.clientTrace = &clientTrace{} rc.Context = rc.clientTrace.createContext(rc.Context) diff --git a/examples/api/main.go b/examples/api/main.go index de7f975..6bdbbd5 100644 --- a/examples/api/main.go +++ b/examples/api/main.go @@ -13,7 +13,7 @@ type GithubApi struct { func NewGithubApi() *GithubApi { config := surf.Config{ BaseURL: "https://api.github.com", - Headers: map[string][]string{ + Header: map[string][]string{ "Accept": {"application/vnd.github+json"}, "X-GitHub-Api-Version": {"2022-11-28"}, }, diff --git a/multipart_file.go b/multipart_file.go index af819a0..a05c931 100644 --- a/multipart_file.go +++ b/multipart_file.go @@ -101,11 +101,7 @@ func (m *multipartFile) Bytes() ([]byte, error) { if len(m.errors) > 0 { // If there are errors, combine them into a single error and return - var errMsg string - for _, e := range m.errors { - errMsg += e.Error() + "; " - } - return nil, errors.New(errMsg[:len(errMsg)-2]) // Removing trailing "; " + return nil, errors.Join(m.errors...) } return m.data.Bytes(), nil diff --git a/options.go b/options.go index ac3206d..f1c9489 100644 --- a/options.go +++ b/options.go @@ -26,10 +26,10 @@ func WithBaseURL(url string) WithRequestConfig { } } -// WithHeaders sets the request headers in the request configuration. -func WithHeaders(header http.Header) WithRequestConfig { +// WithHeader sets the request header in the request configuration. +func WithHeader(header http.Header) WithRequestConfig { return func(c *RequestConfig) { - c.Headers = header + c.Header = header } } diff --git a/response.go b/response.go index d28e39d..60592d1 100644 --- a/response.go +++ b/response.go @@ -2,8 +2,6 @@ package surf import ( "bytes" - "encoding/json" - "encoding/xml" "io" "net/http" "os" @@ -33,12 +31,12 @@ func (r *Response) BodyReader() io.Reader { // Json parses the JSON response body and stores the result in the provided variable (v). func (r *Response) Json(v interface{}) error { - return json.Unmarshal(r.body, &v) + return r.config.JSONUnmarshal(r.body, &v) } // XML parses the xml response body and stores the result in the provided variable (v). func (r *Response) XML(v interface{}) error { - return xml.Unmarshal(r.body, &v) + return r.config.XMLUnmarshal(r.body, &v) } // Text returns the response body as a string. diff --git a/surf.go b/surf.go index 8542a8d..6ca5487 100644 --- a/surf.go +++ b/surf.go @@ -48,7 +48,7 @@ func (s *Surf) prepareRequest(config *RequestConfig) (*http.Request, error) { config.Request = req // Update global Headers Cookies - for key, values := range s.Config.Headers { + for key, values := range s.Config.Header { for _, value := range values { req.Header.Set(key, value) } @@ -83,7 +83,7 @@ func (s *Surf) prepareRequest(config *RequestConfig) (*http.Request, error) { } // Update Request Headers - for key, values := range config.Headers { + for key, values := range config.Header { for _, value := range values { req.Header.Add(key, value) } @@ -212,10 +212,10 @@ func (s *Surf) Request(config *RequestConfig) (*Response, error) { // Upload performs a file upload using the provided URL, file, and optional request configuration. func (s *Surf) Upload(url string, file *multipartFile, args ...WithRequestConfig) (resp *Response, err error) { - return s.makeRequest(url, http.MethodPost, - append(WithRequestConfigChain{ - WithBody(file), - }, args...)..., + return s.makeRequest( + url, + http.MethodPost, + append(WithRequestConfigChain{WithBody(file)}, args...)..., ) } @@ -271,7 +271,7 @@ func (s *Surf) Trace(url string, args ...WithRequestConfig) (*Response, error) { func (s *Surf) CloneDefaultConfig() *Config { return &Config{ BaseURL: s.Config.BaseURL, - Headers: s.Config.Headers.Clone(), + Header: s.Config.Header.Clone(), Timeout: s.Config.Timeout, Params: cloneMap(s.Config.Params), Query: cloneURLValues(s.Config.Query), @@ -283,5 +283,9 @@ func (s *Surf) CloneDefaultConfig() *Config { MaxBodyLength: s.Config.MaxBodyLength, MaxRedirects: s.Config.MaxRedirects, Client: s.Config.Client, + JSONMarshal: s.Config.JSONMarshal, + JSONUnmarshal: s.Config.JSONUnmarshal, + XMLMarshal: s.Config.XMLMarshal, + XMLUnmarshal: s.Config.XMLUnmarshal, } } diff --git a/utils.go b/utils.go index ff05bd8..fd50db6 100644 --- a/utils.go +++ b/utils.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/url" + "reflect" "strconv" "github.com/dsnet/compress/brotli" @@ -100,3 +101,19 @@ func cloneURLValues(originalValues url.Values) url.Values { return clonedValues } + +func isZero(value interface{}) bool { + if value == nil { + return true + } + + v := reflect.ValueOf(value) + return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) +} + +func defaultValue[T any](value, defaultValue T) T { + if isZero(value) { + return defaultValue + } + return value +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..653313d --- /dev/null +++ b/utils_test.go @@ -0,0 +1,24 @@ +package surf + +import ( + "testing" +) + +func TestIsZero(t *testing.T) { + if bl := isZero(nil); !bl { + t.Fail() + } + if bl := isZero(make(map[string]string)); bl { + t.Fail() + } + var mapVal map[string]string + if bl := isZero(mapVal); !bl { + t.Fail() + } + if bl := isZero(""); !bl { + t.Fail() + } + if bl := isZero(0); !bl { + t.Fail() + } +}