diff --git a/pkg/uhttp/wrapper.go b/pkg/uhttp/wrapper.go new file mode 100644 index 00000000..c37db238 --- /dev/null +++ b/pkg/uhttp/wrapper.go @@ -0,0 +1,110 @@ +package uhttp + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" +) + +type ( + HttpClient interface { + HttpClient() *http.Client + Do(req *http.Request, options ...DoOption) (*http.Response, error) + NewRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) (*http.Request, error) + } + BaseHttpClient struct { + httpClient *http.Client + } + + DoOption func(*http.Response) error + RequestOption func() (io.ReadWriter, map[string]string, error) +) + +func WithJSONResponse(response interface{}) DoOption { + return func(resp *http.Response) error { + defer resp.Body.Close() + return json.NewDecoder(resp.Body).Decode(response) + } +} + +func (c *BaseHttpClient) Do(req *http.Request, options ...DoOption) (*http.Response, error) { + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + + for _, option := range options { + err = option(resp) + if err != nil { + return nil, err + } + } + + return resp, err +} + +func WithJSONBody(body interface{}) RequestOption { + return func() (io.ReadWriter, map[string]string, error) { + buffer := new(bytes.Buffer) + err := json.NewEncoder(buffer).Encode(body) + if err != nil { + return nil, nil, err + } + + _, headers, err := WithContentTypeJSONHeader()() + if err != nil { + return nil, nil, err + } + + return buffer, headers, nil + } +} + +func WithAcceptJSONHeader() RequestOption { + return func() (io.ReadWriter, map[string]string, error) { + return nil, map[string]string{ + "Accept": "application/json", + }, nil + } +} + +func WithContentTypeJSONHeader() RequestOption { + return func() (io.ReadWriter, map[string]string, error) { + return nil, map[string]string{ + "Content-Type": "application/json", + }, nil + } +} + +func (c *BaseHttpClient) NewRequest(ctx context.Context, method string, url *url.URL, options ...RequestOption) (*http.Request, error) { + var buffer io.ReadWriter + var headers map[string]string = make(map[string]string) + for _, option := range options { + buf, h, err := option() + if err != nil { + return nil, err + } + + if buf != nil { + buffer = buf + } + + for k, v := range h { + headers[k] = v + } + } + + req, err := http.NewRequestWithContext(ctx, method, url.String(), buffer) + if err != nil { + return nil, err + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + return req, nil +} diff --git a/pkg/uhttp/wrapper_test.go b/pkg/uhttp/wrapper_test.go new file mode 100644 index 00000000..e6057ebe --- /dev/null +++ b/pkg/uhttp/wrapper_test.go @@ -0,0 +1,151 @@ +package uhttp + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +type example struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func TestWrapper_WithJSONBody(t *testing.T) { + exampleBody := example{ + Name: "John", + Age: 30, + } + exampleBodyBuffer := new(bytes.Buffer) + err := json.NewEncoder(exampleBodyBuffer).Encode(exampleBody) + if err != nil { + t.Fatal(err) + } + + option := WithJSONBody(exampleBody) + buffer, _, err := option() + + require.Nil(t, err) + require.Equal(t, exampleBodyBuffer, buffer) +} + +func TestWrapper_WithAcceptJSONHeader(t *testing.T) { + option := WithAcceptJSONHeader() + buffer, headers, err := option() + + require.Nil(t, err) + require.Nil(t, buffer) + require.Contains(t, headers, "Accept") + require.Equal(t, "application/json", headers["Accept"]) +} + +func TestWrapper_WithContentTypeJSONHeader(t *testing.T) { + option := WithContentTypeJSONHeader() + buffer, headers, err := option() + + require.Nil(t, err) + require.Nil(t, buffer) + require.Contains(t, headers, "Content-Type") + require.Equal(t, "application/json", headers["Content-Type"]) +} + +func TestWrapper_WithJSONResponse(t *testing.T) { + exampleResponse := example{ + Name: "John", + Age: 30, + } + exampleResponseBuffer := new(bytes.Buffer) + err := json.NewEncoder(exampleResponseBuffer).Encode(exampleResponse) + if err != nil { + t.Fatal(err) + } + + resp := http.Response{ + Body: io.NopCloser(exampleResponseBuffer), + } + + responseBody := example{} + option := WithJSONResponse(&responseBody) + err = option(&resp) + + require.Nil(t, err) + require.Equal(t, exampleResponse, responseBody) +} + +func TestWrapper_NewRequest(t *testing.T) { + type expected struct { + method string + url string + headers http.Header + body io.ReadCloser + err error + } + + exampleBody := example{Name: "John", Age: 30} + exampleBodyBuffer := new(bytes.Buffer) + err := json.NewEncoder(exampleBodyBuffer).Encode(exampleBody) + if err != nil { + t.Fatal(err) + } + + test := []struct { + name string + method string + url string + options []RequestOption + expected expected + }{ + { + name: "GET request with no options", + method: http.MethodGet, + url: "http://example.com", + options: nil, + expected: expected{ + method: http.MethodGet, + url: "http://example.com", + headers: http.Header{}, + body: nil, + err: nil, + }, + }, + { + name: "POST request with JSON body", + method: http.MethodPost, + url: "http://example.com", + options: []RequestOption{WithJSONBody(exampleBody), WithAcceptJSONHeader()}, + expected: expected{ + method: http.MethodPost, + url: "http://example.com", + headers: http.Header{"Accept": []string{"application/json"}, "Content-Type": []string{"application/json"}}, + body: io.NopCloser(exampleBodyBuffer), + err: nil, + }, + }, + } + + for _, tc := range test { + t.Run(tc.name, func(t *testing.T) { + u, err := url.Parse(tc.url) + if err != nil { + t.Fatal(err) + } + + client := BaseHttpClient{ + httpClient: http.DefaultClient, + } + + req, err := client.NewRequest(context.Background(), tc.method, u, tc.options...) + require.Equal(t, tc.expected.err, err) + require.Equal(t, tc.expected.method, req.Method) + require.Equal(t, tc.expected.url, req.URL.String()) + require.Equal(t, tc.expected.headers, req.Header) + require.Equal(t, tc.expected.body, req.Body) + }) + } +}