Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 0ad4402

Browse files
author
Noah Lee
authored
Add the api package to interact with a server (#318)
* Add api package * Add unit tests * Comment unused types
1 parent 7b68e24 commit 0ad4402

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

pkg/api/client.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package api
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"io/ioutil"
10+
"net/http"
11+
"net/url"
12+
"strings"
13+
14+
"github.com/gitploy-io/gitploy/pkg/e"
15+
)
16+
17+
type (
18+
Client struct {
19+
// Reuse a single struct instead of allocating one for each service on the heap.
20+
common *client
21+
22+
// Services used for talking to different parts of the Gitploy API.
23+
}
24+
25+
client struct {
26+
// HTTP client used to communicate with the API.
27+
httpClient *http.Client
28+
29+
// Base URL for API requests. BaseURL should
30+
// always be specified with a trailing slash.
31+
BaseURL *url.URL
32+
}
33+
34+
// service struct {
35+
// client *client
36+
// }
37+
38+
ErrorResponse struct {
39+
Code string `json:"code"`
40+
Message string `json:"message"`
41+
}
42+
)
43+
44+
func NewClient(host string, httpClient *http.Client) *Client {
45+
if httpClient == nil {
46+
httpClient = &http.Client{}
47+
}
48+
49+
baseURL, _ := url.Parse(host)
50+
51+
c := &Client{
52+
common: &client{httpClient: httpClient, BaseURL: baseURL},
53+
}
54+
55+
return c
56+
}
57+
58+
// NewRequest creates an API request. A relative URL can be provided in urlStr,
59+
// in which case it is resolved relative to the BaseURL of the Client.
60+
// Relative URLs should always be specified without a preceding slash. If
61+
// specified, the value pointed to by body is JSON encoded and included as the
62+
// request body.
63+
func (c *client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
64+
if !strings.HasSuffix(c.BaseURL.Path, "/") {
65+
return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
66+
}
67+
u, err := c.BaseURL.Parse(urlStr)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
var buf io.ReadWriter
73+
if body != nil {
74+
buf = &bytes.Buffer{}
75+
enc := json.NewEncoder(buf)
76+
enc.SetEscapeHTML(false)
77+
err := enc.Encode(body)
78+
if err != nil {
79+
return nil, err
80+
}
81+
}
82+
83+
req, err := http.NewRequest(method, u.String(), buf)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
if body != nil {
89+
req.Header.Set("Content-Type", "application/json")
90+
}
91+
92+
return req, nil
93+
}
94+
95+
// Do sends an API request and returns the API response. The API response is
96+
// JSON decoded and stored in the value pointed to by v, or returned as an
97+
// error if an API error has occurred.
98+
func (c *client) Do(ctx context.Context, req *http.Request, v interface{}) error {
99+
if ctx == nil {
100+
return fmt.Errorf("There is no context")
101+
}
102+
103+
res, err := c.httpClient.Do(req)
104+
if err != nil {
105+
return fmt.Errorf("Failed to request: %w", err)
106+
}
107+
108+
defer res.Body.Close()
109+
110+
// Return internal errors
111+
if res.StatusCode > 299 {
112+
errRes := &ErrorResponse{}
113+
114+
out, _ := ioutil.ReadAll(res.Body)
115+
if err := json.Unmarshal(out, errRes); err != nil {
116+
return fmt.Errorf("Failed to parse an error response: %w", err)
117+
}
118+
119+
return e.NewErrorWithMessage(e.ErrorCode(errRes.Code), errRes.Message, nil)
120+
}
121+
122+
if v != nil {
123+
return json.NewDecoder(res.Body).Decode(v)
124+
}
125+
126+
return nil
127+
}

pkg/api/client_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
"net/url"
9+
"testing"
10+
11+
"github.com/gitploy-io/gitploy/pkg/e"
12+
)
13+
14+
func TestClient_Do(t *testing.T) {
15+
t.Run("Return an internal error", func(t *testing.T) {
16+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
w.WriteHeader(http.StatusNotFound)
18+
fmt.Fprint(w, `{"code": "entity_not_found", "message": "It is not found."}`)
19+
}))
20+
defer ts.Close()
21+
22+
// Append '/' to avoid an trailing slash error.
23+
baseURL, _ := url.Parse(ts.URL + "/")
24+
c := &client{httpClient: http.DefaultClient, BaseURL: baseURL}
25+
26+
req, err := c.NewRequest("GET", baseURL.Path, nil)
27+
if err != nil {
28+
t.Fatalf("Failed to build a request: %s", err)
29+
}
30+
31+
err = c.Do(context.Background(), req, nil)
32+
if !e.HasErrorCode(err, e.ErrorCodeEntityNotFound) {
33+
t.Fatalf("Do = %v, want ErrorCodeEntityNotFound", err)
34+
}
35+
})
36+
37+
t.Run("Return OK response", func(t *testing.T) {
38+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39+
w.WriteHeader(http.StatusOK)
40+
}))
41+
defer ts.Close()
42+
43+
// Append '/' to avoid an trailing slash error.
44+
baseURL, _ := url.Parse(ts.URL + "/")
45+
c := &client{httpClient: http.DefaultClient, BaseURL: baseURL}
46+
47+
req, err := c.NewRequest("GET", baseURL.Path, nil)
48+
if err != nil {
49+
t.Fatalf("Failed to build a request: %s", err)
50+
}
51+
52+
err = c.Do(context.Background(), req, nil)
53+
if err != nil {
54+
t.Fatalf("Do returns an error: %s", err)
55+
}
56+
})
57+
}

0 commit comments

Comments
 (0)