import (
"fmt"
restRepo "cooperative.bakode.xyz/internal/repository/rest"
)
const baseURL := "http://test.api/v1"
rest := restRepo.New(baseURL)
resp, err := rest.Post(context.Background(), "", rest.WithBody(body),
rest.WithHeader("Content-Type", "application/json"))
fmt.Println(resp, err)
Last active
February 10, 2026 07:30
-
-
Save aasumitro/2459aebf1b66c6f884fb0461c8aba3a6 to your computer and use it in GitHub Desktop.
REST Repo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package rest | |
| import ( | |
| "bytes" | |
| "context" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "time" | |
| "go.opentelemetry.io/otel" | |
| ) | |
| type ( | |
| IRESTRepository interface { | |
| Get(ctx context.Context, endpoint string, opts ...Option) (*Response, error) | |
| Post(ctx context.Context, endpoint string, opts ...Option) (*Response, error) | |
| Put(ctx context.Context, endpoint string, opts ...Option) (*Response, error) | |
| Patch(ctx context.Context, endpoint string, opts ...Option) (*Response, error) | |
| Delete(ctx context.Context, endpoint string, opts ...Option) (*Response, error) | |
| } | |
| // Client is a struct who has baseURL property | |
| Client struct { | |
| baseURL string | |
| httpClient *http.Client | |
| headers map[string]Header | |
| query map[string]string | |
| body []byte | |
| timeout time.Duration | |
| } | |
| Header struct { | |
| Value string | |
| IsDefault bool | |
| } | |
| ) | |
| const ( | |
| DefaultTimeout = 10 * time.Second | |
| namespace = "cooperative.bakode.xyz/internal/repository/rest" | |
| ) | |
| var tracer = otel.Tracer(namespace) | |
| // New func returns a Client struct | |
| func New(baseURL string, opts ...ClientOption) *Client { | |
| httpClient := &http.Client{Timeout: DefaultTimeout} | |
| client := &Client{httpClient: httpClient, baseURL: baseURL, timeout: DefaultTimeout} | |
| for _, opt := range opts { | |
| opt(client) | |
| } | |
| return client | |
| } | |
| // Get func returns a request | |
| func (c *Client) Get(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { | |
| closeOption := c.initOpts(opts...) | |
| defer closeOption() | |
| req, err := http.NewRequestWithContext(ctx, | |
| http.MethodGet, c.baseURL+endpoint, http.NoBody) | |
| if err != nil { | |
| return nil, err | |
| } | |
| prepReq := c.prepareReq(req) | |
| return c.sendReq(ctx, prepReq) | |
| } | |
| // Post func returns a request | |
| func (c *Client) Post(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { | |
| closeOption := c.initOpts(opts...) | |
| defer closeOption() | |
| req, err := http.NewRequestWithContext(ctx, http.MethodPost, | |
| c.baseURL+endpoint, bytes.NewBuffer(c.body)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| prepReq := c.prepareReq(req) | |
| return c.sendReq(ctx, prepReq) | |
| } | |
| // Put func returns a request | |
| func (c *Client) Put(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { | |
| closeOption := c.initOpts(opts...) | |
| defer closeOption() | |
| req, err := http.NewRequestWithContext(ctx, http.MethodPut, | |
| c.baseURL+endpoint, bytes.NewBuffer(c.body)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| prepReq := c.prepareReq(req) | |
| return c.sendReq(ctx, prepReq) | |
| } | |
| // Patch func returns a request | |
| func (c *Client) Patch(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { | |
| closeOption := c.initOpts(opts...) | |
| defer closeOption() | |
| req, err := http.NewRequestWithContext(ctx, http.MethodPatch, | |
| c.baseURL+endpoint, bytes.NewBuffer(c.body)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| prepReq := c.prepareReq(req) | |
| return c.sendReq(ctx, prepReq) | |
| } | |
| // Delete func returns a request | |
| func (c *Client) Delete(ctx context.Context, endpoint string, opts ...Option) (*Response, error) { | |
| closeOption := c.initOpts(opts...) | |
| defer closeOption() | |
| req, err := http.NewRequestWithContext(ctx, http.MethodDelete, | |
| c.baseURL+endpoint, bytes.NewBuffer(c.body)) | |
| if err != nil { | |
| return nil, err | |
| } | |
| prepReq := c.prepareReq(req) | |
| return c.sendReq(ctx, prepReq) | |
| } | |
| func (c *Client) initOpts(opts ...Option) func() { | |
| for _, opt := range opts { | |
| opt(c) | |
| } | |
| return func() { | |
| for key, header := range c.headers { | |
| if !header.IsDefault { | |
| delete(c.headers, key) | |
| } | |
| } | |
| c.query = make(map[string]string) | |
| c.body = nil | |
| } | |
| } | |
| func (c *Client) prepareReq(req *http.Request) *http.Request { | |
| // set headers | |
| for key, header := range c.headers { | |
| req.Header.Set(key, header.Value) | |
| } | |
| // set query | |
| q := req.URL.Query() | |
| for key, value := range c.query { | |
| q.Add(key, value) | |
| } | |
| req.URL.RawQuery = q.Encode() | |
| return req | |
| } | |
| func (c *Client) sendReq(ctx context.Context, req *http.Request) (*Response, error) { | |
| ctx, span := tracer.Start(ctx, fmt.Sprintf("%s SendRequest", req.Method)) | |
| defer span.End() | |
| reqCtx, cancel := context.WithTimeout(ctx, c.timeout) | |
| defer cancel() | |
| res, err := c.httpClient.Do(req.WithContext(reqCtx)) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to send request %v", err) | |
| } | |
| defer func() { _ = res.Body.Close() }() | |
| body, err := io.ReadAll(res.Body) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to read response body %v", err) | |
| } | |
| return &Response{res, body}, nil | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package rest | |
| import ( | |
| "net/http" | |
| "time" | |
| ) | |
| type ( | |
| Option func(c *Client) | |
| ClientOption Option | |
| ) | |
| func WithCustomHTTPClient(client *http.Client) ClientOption { | |
| return func(c *Client) { | |
| c.httpClient = client | |
| } | |
| } | |
| func WithDefaultHeaders() ClientOption { | |
| return func(c *Client) { | |
| if c.headers == nil { | |
| c.headers = make(map[string]Header) | |
| } | |
| c.headers["Content-Type"] = Header{Value: "application/json", IsDefault: true} | |
| c.headers["Accept"] = Header{Value: "application/json", IsDefault: true} | |
| } | |
| } | |
| func WithTimeout(timeout time.Duration) ClientOption { | |
| return func(c *Client) { | |
| c.timeout = timeout | |
| c.httpClient.Timeout = timeout | |
| } | |
| } | |
| func WithHeader(key, value string) Option { | |
| return func(c *Client) { | |
| if c.headers == nil { | |
| c.headers = make(map[string]Header) | |
| } | |
| c.headers[key] = Header{Value: value, IsDefault: false} | |
| } | |
| } | |
| func WithQuery(key, value string) Option { | |
| return func(c *Client) { | |
| if c.query == nil { | |
| c.query = make(map[string]string) | |
| } | |
| c.query[key] = value | |
| } | |
| } | |
| func WithBody(body []byte) Option { | |
| return func(c *Client) { | |
| c.body = body | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package rest | |
| import ( | |
| "encoding/json" | |
| "net/http" | |
| ) | |
| type Response struct { | |
| res *http.Response | |
| body []byte | |
| } | |
| func (r *Response) Body() []byte { | |
| return r.body | |
| } | |
| func (r *Response) Unmarshal(v any) error { | |
| return json.Unmarshal(r.body, &v) | |
| } | |
| func (r *Response) Status() int { | |
| return r.res.StatusCode | |
| } | |
| func (r *Response) Headers() http.Header { | |
| return r.res.Header | |
| } | |
| func (r *Response) Cookies() []*http.Cookie { | |
| return r.res.Cookies() | |
| } | |
| func (r *Response) OK() bool { | |
| return r.res.StatusCode >= 200 && r.res.StatusCode <= 299 | |
| } | |
| func (r *Response) Get() *http.Response { | |
| return r.res | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package rest_test | |
| import ( | |
| "context" | |
| "net/http" | |
| "net/http/httptest" | |
| "testing" | |
| "cooperative.bakode.xyz/internal/repository/rest" | |
| "github.com/stretchr/testify/suite" | |
| ) | |
| type TestClientSuite struct { | |
| suite.Suite | |
| ctx context.Context | |
| } | |
| type TestMethod struct { | |
| name, baseURL string | |
| method func(ctx context.Context, endpoint string, opts ...rest.Option) (*rest.Response, error) | |
| options []rest.Option | |
| } | |
| func TestClient(t *testing.T) { | |
| suite.Run(t, new(TestClientSuite)) | |
| } | |
| func (s *TestClientSuite) SetupSuite() { | |
| s.ctx = context.Background() | |
| } | |
| func (s *TestClientSuite) Test_New_ShouldRunSuccessfully() { | |
| baseURL := "http://localhost:8080" | |
| customClient := &http.Client{} | |
| client := rest.New(baseURL, rest.WithCustomHTTPClient(customClient)) | |
| s.NotNil(client) | |
| } | |
| func (s *TestClientSuite) Test_Request_WhenRequestIsInvalid_ShouldReturnError() { | |
| baseURL := "http://localhost:8080" | |
| client := rest.New(baseURL) | |
| requests := []TestMethod{ | |
| { | |
| name: "GET", | |
| baseURL: baseURL, | |
| method: client.Get, | |
| }, | |
| { | |
| name: "POST", | |
| baseURL: baseURL, | |
| method: client.Post, | |
| }, | |
| { | |
| name: "PUT", | |
| baseURL: baseURL, | |
| method: client.Put, | |
| }, | |
| { | |
| name: "PATCH", | |
| baseURL: baseURL, | |
| method: client.Patch, | |
| }, | |
| { | |
| name: "DELETE", | |
| baseURL: baseURL, | |
| method: client.Delete, | |
| }, | |
| } | |
| for _, req := range requests { | |
| s.Run(req.name, func() { | |
| response, err := req.method(nil, "") | |
| s.Nil(response) | |
| s.Error(err) | |
| }) | |
| } | |
| } | |
| func (s *TestClientSuite) Test_Request_WhenDoReturnsAnError_ShouldReturnError() { | |
| baseURLWithInvalidSchema := "htt \\`" | |
| client := rest.New( | |
| baseURLWithInvalidSchema, | |
| rest.WithTimeout(0), | |
| rest.WithDefaultHeaders(), | |
| ) | |
| requests := []TestMethod{ | |
| { | |
| name: "GET", | |
| baseURL: baseURLWithInvalidSchema, | |
| method: client.Get, | |
| }, | |
| { | |
| name: "POST", | |
| baseURL: baseURLWithInvalidSchema, | |
| method: client.Post, | |
| }, | |
| { | |
| name: "PUT", | |
| baseURL: baseURLWithInvalidSchema, | |
| method: client.Put, | |
| }, | |
| { | |
| name: "PATCH", | |
| baseURL: baseURLWithInvalidSchema, | |
| method: client.Patch, | |
| }, | |
| { | |
| name: "DELETE", | |
| baseURL: baseURLWithInvalidSchema, | |
| method: client.Delete, | |
| }, | |
| } | |
| for _, req := range requests { | |
| s.Run(req.name, func() { | |
| response, err := req.method(s.ctx, "") | |
| s.Nil(response) | |
| s.Error(err) | |
| }) | |
| } | |
| } | |
| func (s *TestClientSuite) Test_Request_WhenBodyReturnsError_ShouldReturnError() { | |
| svc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| w.Header().Set("Content-Length", "1") | |
| _, _ = w.Write(nil) | |
| })) | |
| defer svc.Close() | |
| client := rest.New(svc.URL) | |
| requests := []TestMethod{ | |
| { | |
| name: "GET", | |
| baseURL: svc.URL, | |
| method: client.Get, | |
| }, | |
| { | |
| name: "POST", | |
| baseURL: svc.URL, | |
| method: client.Post, | |
| }, | |
| { | |
| name: "PUT", | |
| baseURL: svc.URL, | |
| method: client.Put, | |
| }, | |
| { | |
| name: "PATCH", | |
| baseURL: svc.URL, | |
| method: client.Patch, | |
| }, | |
| { | |
| name: "DELETE", | |
| baseURL: svc.URL, | |
| method: client.Delete, | |
| }, | |
| } | |
| for _, req := range requests { | |
| s.Run(req.name, func() { | |
| response, err := req.method(s.ctx, "") | |
| s.Nil(response) | |
| s.Error(err) | |
| }) | |
| } | |
| } | |
| func (s *TestClientSuite) Test_Request_ShouldRunSuccessfully() { | |
| svc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| w.WriteHeader(http.StatusOK) | |
| })) | |
| defer svc.Close() | |
| client := rest.New(svc.URL) | |
| requests := []TestMethod{ | |
| { | |
| name: "GET", | |
| baseURL: svc.URL, | |
| method: client.Get, | |
| }, | |
| { | |
| name: "POST", | |
| baseURL: svc.URL, | |
| method: client.Post, | |
| }, | |
| { | |
| name: "PUT", | |
| baseURL: svc.URL, | |
| method: client.Put, | |
| }, | |
| { | |
| name: "PATCH", | |
| baseURL: svc.URL, | |
| method: client.Patch, | |
| }, | |
| { | |
| name: "DELETE", | |
| baseURL: svc.URL, | |
| method: client.Delete, | |
| }, | |
| } | |
| for _, req := range requests { | |
| s.Run(req.name, func() { | |
| response, err := req.method(s.ctx, "") | |
| s.NotNil(response) | |
| s.NoError(err) | |
| }) | |
| } | |
| } | |
| func (s *TestClientSuite) Test_Request_WithOptions_ShouldRunSuccessfully() { | |
| svc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { | |
| w.WriteHeader(http.StatusOK) | |
| })) | |
| defer svc.Close() | |
| client := rest.New(svc.URL) | |
| requests := []TestMethod{ | |
| { | |
| name: "GET", | |
| baseURL: svc.URL, | |
| method: client.Get, | |
| options: []rest.Option{ | |
| rest.WithHeader("key", "value"), | |
| rest.WithQuery("key", "value"), | |
| }, | |
| }, | |
| { | |
| name: "POST", | |
| baseURL: svc.URL, | |
| method: client.Post, | |
| options: []rest.Option{ | |
| rest.WithHeader("key", "value"), | |
| rest.WithQuery("key", "value"), | |
| rest.WithBody([]byte("body")), | |
| }, | |
| }, | |
| { | |
| name: "PUT", | |
| baseURL: svc.URL, | |
| method: client.Put, | |
| options: []rest.Option{rest.WithHeader("key", "value"), rest.WithQuery("key", "value")}, | |
| }, | |
| { | |
| name: "PATCH", | |
| baseURL: svc.URL, | |
| method: client.Patch, | |
| options: []rest.Option{rest.WithHeader("key", "value"), rest.WithQuery("key", "value")}, | |
| }, | |
| { | |
| name: "DELETE", | |
| baseURL: svc.URL, | |
| method: client.Delete, | |
| options: []rest.Option{rest.WithHeader("key", "value"), rest.WithQuery("key", "value")}, | |
| }, | |
| } | |
| for _, req := range requests { | |
| s.Run(req.name, func() { | |
| response, err := req.method(s.ctx, "", req.options...) | |
| s.NotNil(response) | |
| s.NoError(err) | |
| s.NotNil(response.Body()) | |
| s.Equal(response.Status(), http.StatusOK) | |
| s.True(response.OK()) | |
| s.NotNil(response.Get()) | |
| s.NotNil(response.Cookies()) | |
| s.NotNil(response.Headers()) | |
| var data any | |
| s.Error(response.Unmarshal(&data)) | |
| }) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment