Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions pkg/api/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package api

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"

"github.com/gitploy-io/gitploy/pkg/e"
)

type (
Client struct {
// Reuse a single struct instead of allocating one for each service on the heap.
common *client

// Services used for talking to different parts of the Gitploy API.
}

client struct {
// HTTP client used to communicate with the API.
httpClient *http.Client

// Base URL for API requests. BaseURL should
// always be specified with a trailing slash.
BaseURL *url.URL
}

// service struct {
// client *client
// }

ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}
)

func NewClient(host string, httpClient *http.Client) *Client {
if httpClient == nil {
httpClient = &http.Client{}
}

baseURL, _ := url.Parse(host)

c := &Client{
common: &client{httpClient: httpClient, BaseURL: baseURL},
}

return c
}

// NewRequest creates an API request. A relative URL can be provided in urlStr,
// in which case it is resolved relative to the BaseURL of the Client.
// Relative URLs should always be specified without a preceding slash. If
// specified, the value pointed to by body is JSON encoded and included as the
// request body.
func (c *client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
if !strings.HasSuffix(c.BaseURL.Path, "/") {
return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
}
u, err := c.BaseURL.Parse(urlStr)
if err != nil {
return nil, err
}

var buf io.ReadWriter
if body != nil {
buf = &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
err := enc.Encode(body)
if err != nil {
return nil, err
}
}

req, err := http.NewRequest(method, u.String(), buf)
if err != nil {
return nil, err
}

if body != nil {
req.Header.Set("Content-Type", "application/json")
}

return req, nil
}

// Do sends an API request and returns the API response. The API response is
// JSON decoded and stored in the value pointed to by v, or returned as an
// error if an API error has occurred.
func (c *client) Do(ctx context.Context, req *http.Request, v interface{}) error {
if ctx == nil {
return fmt.Errorf("There is no context")
}

res, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to request: %w", err)
}

defer res.Body.Close()

// Return internal errors
if res.StatusCode > 299 {
errRes := &ErrorResponse{}

out, _ := ioutil.ReadAll(res.Body)
if err := json.Unmarshal(out, errRes); err != nil {
return fmt.Errorf("Failed to parse an error response: %w", err)
}

return e.NewErrorWithMessage(e.ErrorCode(errRes.Code), errRes.Message, nil)
}

if v != nil {
return json.NewDecoder(res.Body).Decode(v)
}

return nil
}
57 changes: 57 additions & 0 deletions pkg/api/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package api

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/gitploy-io/gitploy/pkg/e"
)

func TestClient_Do(t *testing.T) {
t.Run("Return an internal error", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprint(w, `{"code": "entity_not_found", "message": "It is not found."}`)
}))
defer ts.Close()

// Append '/' to avoid an trailing slash error.
baseURL, _ := url.Parse(ts.URL + "/")
c := &client{httpClient: http.DefaultClient, BaseURL: baseURL}

req, err := c.NewRequest("GET", baseURL.Path, nil)
if err != nil {
t.Fatalf("Failed to build a request: %s", err)
}

err = c.Do(context.Background(), req, nil)
if !e.HasErrorCode(err, e.ErrorCodeEntityNotFound) {
t.Fatalf("Do = %v, want ErrorCodeEntityNotFound", err)
}
})

t.Run("Return OK response", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()

// Append '/' to avoid an trailing slash error.
baseURL, _ := url.Parse(ts.URL + "/")
c := &client{httpClient: http.DefaultClient, BaseURL: baseURL}

req, err := c.NewRequest("GET", baseURL.Path, nil)
if err != nil {
t.Fatalf("Failed to build a request: %s", err)
}

err = c.Do(context.Background(), req, nil)
if err != nil {
t.Fatalf("Do returns an error: %s", err)
}
})
}