Skip to content

Commit f02088c

Browse files
authored
refactor: api client uses functional options, debug routes to io.Writer (#66)
1 parent d05858b commit f02088c

11 files changed

Lines changed: 74 additions & 63 deletions

cmd/root.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ var teamFlag string
1919
var debugFlag bool
2020

2121
func newAPIClient(cfg *config.Config) *api.Client {
22-
return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).
23-
WithUserAgent("loops-cli/" + version)
22+
opts := []api.Option{
23+
api.WithBaseURL(cfg.EndpointURL),
24+
api.WithUserAgent("loops-cli/" + version),
25+
}
26+
if cfg.Debug {
27+
opts = append(opts, api.WithLogger(os.Stderr))
28+
}
29+
return api.NewClient(cfg.APIKey, opts...)
2430
}
2531

2632
func loadConfig() (*config.Config, error) {

internal/api/api_key_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestGetAPIKey(t *testing.T) {
5151
}))
5252
defer server.Close()
5353

54-
client := NewClient(server.URL, "test-key", false)
54+
client := NewClient("test-key", WithBaseURL(server.URL))
5555
result, err := client.GetAPIKey()
5656

5757
if tt.wantAPIErr != nil {

internal/api/campaigns_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func TestCreateCampaign(t *testing.T) {
8888
}))
8989
defer server.Close()
9090

91-
client := NewClient(server.URL, "test-key", false)
91+
client := NewClient("test-key", WithBaseURL(server.URL))
9292
resp, err := client.CreateCampaign(CreateCampaignRequest{Name: "Spring Launch"})
9393

9494
if tt.wantAPIErr != nil {
@@ -141,7 +141,7 @@ func TestCreateCampaign_RequestBody(t *testing.T) {
141141
}))
142142
defer server.Close()
143143

144-
client := NewClient(server.URL, "test-key", false)
144+
client := NewClient("test-key", WithBaseURL(server.URL))
145145
if _, err := client.CreateCampaign(CreateCampaignRequest{Name: "Spring"}); err != nil {
146146
t.Fatalf("unexpected error: %v", err)
147147
}
@@ -205,7 +205,7 @@ func TestUpdateCampaign(t *testing.T) {
205205
}))
206206
defer server.Close()
207207

208-
client := NewClient(server.URL, "test-key", false)
208+
client := NewClient("test-key", WithBaseURL(server.URL))
209209
result, err := client.UpdateCampaign("cmp_abc123", UpdateCampaignRequest{Name: "Renamed"})
210210

211211
if tt.wantAPIErr != nil {
@@ -261,7 +261,7 @@ func TestUpdateCampaign_RequestBodyAndPath(t *testing.T) {
261261
}))
262262
defer server.Close()
263263

264-
client := NewClient(server.URL, "test-key", false)
264+
client := NewClient("test-key", WithBaseURL(server.URL))
265265
if _, err := client.UpdateCampaign("cmp_abc123", UpdateCampaignRequest{Name: "Renamed"}); err != nil {
266266
t.Fatalf("unexpected error: %v", err)
267267
}
@@ -337,7 +337,7 @@ func TestGetCampaign(t *testing.T) {
337337
}))
338338
defer server.Close()
339339

340-
client := NewClient(server.URL, "test-key", false)
340+
client := NewClient("test-key", WithBaseURL(server.URL))
341341
result, err := client.GetCampaign(tt.id)
342342

343343
if tt.wantAPIErr != nil {
@@ -429,7 +429,7 @@ func TestListCampaigns(t *testing.T) {
429429
}))
430430
defer server.Close()
431431

432-
client := NewClient(server.URL, "test-key", false)
432+
client := NewClient("test-key", WithBaseURL(server.URL))
433433
campaigns, pagination, err := client.ListCampaigns(PaginationParams{})
434434

435435
if tt.wantAPIErr != nil {
@@ -476,7 +476,7 @@ func TestListCampaigns_ResponseData(t *testing.T) {
476476
}))
477477
defer server.Close()
478478

479-
client := NewClient(server.URL, "test-key", false)
479+
client := NewClient("test-key", WithBaseURL(server.URL))
480480
campaigns, _, err := client.ListCampaigns(PaginationParams{})
481481
if err != nil {
482482
t.Fatalf("unexpected error: %v", err)
@@ -526,7 +526,7 @@ func TestListCampaigns_QueryParams(t *testing.T) {
526526
}))
527527
defer server.Close()
528528

529-
client := NewClient(server.URL, "test-key", false)
529+
client := NewClient("test-key", WithBaseURL(server.URL))
530530
client.ListCampaigns(tt.params)
531531

532532
if gotPerPage != tt.wantPerPage {

internal/api/client.go

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import (
77
"io"
88
"math/rand/v2"
99
"net/http"
10-
"os"
1110
"time"
1211
)
1312

1413
const (
15-
maxRetries = 2
16-
baseDelay = 500 * time.Millisecond
14+
DefaultBaseURL = "https://app.loops.so/api/v1"
15+
maxRetries = 2
16+
baseDelay = 500 * time.Millisecond
1717
)
1818

1919
var sleep = time.Sleep
@@ -35,22 +35,27 @@ type Client struct {
3535
baseURL string
3636
apiKey string
3737
httpClient *http.Client
38-
debug bool
38+
logger io.Writer
3939
userAgent string
4040
}
4141

42-
func NewClient(baseURL, apiKey string, debug bool) *Client {
43-
return &Client{
44-
baseURL: baseURL,
42+
type Option func(*Client)
43+
44+
func WithBaseURL(u string) Option { return func(c *Client) { c.baseURL = u } }
45+
func WithUserAgent(ua string) Option { return func(c *Client) { c.userAgent = ua } }
46+
func WithLogger(w io.Writer) Option { return func(c *Client) { c.logger = w } }
47+
func WithHTTPClient(h *http.Client) Option { return func(c *Client) { c.httpClient = h } }
48+
49+
func NewClient(apiKey string, opts ...Option) *Client {
50+
c := &Client{
51+
baseURL: DefaultBaseURL,
4552
apiKey: apiKey,
4653
httpClient: &http.Client{Timeout: 5 * time.Second},
47-
debug: debug,
4854
userAgent: "loops-go/dev",
4955
}
50-
}
51-
52-
func (c *Client) WithUserAgent(ua string) *Client {
53-
c.userAgent = ua
56+
for _, opt := range opts {
57+
opt(c)
58+
}
5459
return c
5560
}
5661

@@ -73,20 +78,20 @@ func errorFromResponse(resp *http.Response) *APIError {
7378
func (c *Client) logResponse(resp *http.Response) {
7479
raw, err := io.ReadAll(resp.Body)
7580
if err != nil {
76-
fmt.Fprintf(os.Stderr, "[debug] Response: %s (body read failed: %v)\n", resp.Status, err)
81+
fmt.Fprintf(c.logger, "[debug] Response: %s (body read failed: %v)\n", resp.Status, err)
7782
resp.Body = io.NopCloser(bytes.NewReader(nil))
7883
return
7984
}
8085
resp.Body = io.NopCloser(bytes.NewReader(raw))
81-
fmt.Fprintf(os.Stderr, "[debug] Response: %s (%d bytes)\n", resp.Status, len(raw))
86+
fmt.Fprintf(c.logger, "[debug] Response: %s (%d bytes)\n", resp.Status, len(raw))
8287
if len(raw) == 0 {
8388
return
8489
}
8590
var pretty bytes.Buffer
8691
if json.Indent(&pretty, raw, "", " ") == nil {
87-
fmt.Fprintf(os.Stderr, "[debug] Body:\n%s\n", pretty.String())
92+
fmt.Fprintf(c.logger, "[debug] Body:\n%s\n", pretty.String())
8893
} else {
89-
fmt.Fprintf(os.Stderr, "[debug] Body: %s\n", raw)
94+
fmt.Fprintf(c.logger, "[debug] Body: %s\n", raw)
9095
}
9196
}
9297

@@ -116,7 +121,7 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
116121
continue
117122
}
118123
if !isRetryable(resp.StatusCode) {
119-
if c.debug {
124+
if c.logger != nil {
120125
c.logResponse(resp)
121126
}
122127
return resp, nil
@@ -135,7 +140,7 @@ func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request,
135140
url := fmt.Sprintf("%s%s", c.baseURL, path)
136141

137142
var bodyBytes []byte
138-
if body != nil && c.debug {
143+
if body != nil && c.logger != nil {
139144
var err error
140145
bodyBytes, err = io.ReadAll(body)
141146
if err != nil {
@@ -154,18 +159,18 @@ func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request,
154159
req.Header.Set("Content-Type", "application/json")
155160
}
156161

157-
if c.debug {
158-
fmt.Fprintf(os.Stderr, "[debug] %s %s\n", method, url)
159-
fmt.Fprintf(os.Stderr, "[debug] Authorization: Bearer [REDACTED]\n")
162+
if c.logger != nil {
163+
fmt.Fprintf(c.logger, "[debug] %s %s\n", method, url)
164+
fmt.Fprintf(c.logger, "[debug] Authorization: Bearer [REDACTED]\n")
160165
if req.Header.Get("Content-Type") != "" {
161-
fmt.Fprintf(os.Stderr, "[debug] Content-Type: %s\n", req.Header.Get("Content-Type"))
166+
fmt.Fprintf(c.logger, "[debug] Content-Type: %s\n", req.Header.Get("Content-Type"))
162167
}
163168
if len(bodyBytes) > 0 {
164169
var pretty bytes.Buffer
165170
if json.Indent(&pretty, bodyBytes, "", " ") == nil {
166-
fmt.Fprintf(os.Stderr, "[debug] Body:\n%s\n", pretty.String())
171+
fmt.Fprintf(c.logger, "[debug] Body:\n%s\n", pretty.String())
167172
} else {
168-
fmt.Fprintf(os.Stderr, "[debug] Body: %s\n", bodyBytes)
173+
fmt.Fprintf(c.logger, "[debug] Body: %s\n", bodyBytes)
169174
}
170175
}
171176
}

internal/api/client_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestDo_RetryResetsBody(t *testing.T) {
2929
}))
3030
defer server.Close()
3131

32-
client := NewClient(server.URL, "test-key", false)
32+
client := NewClient("test-key", WithBaseURL(server.URL))
3333
req, _ := client.newRequest(http.MethodPost, "/", bytes.NewReader([]byte(`{"hello":"world"}`)))
3434
resp, err := client.do(req)
3535
if err != nil {
@@ -48,7 +48,7 @@ func TestDo_RetryResetsBody(t *testing.T) {
4848
}
4949

5050
func TestNewRequest(t *testing.T) {
51-
client := NewClient("https://example.com/api/v1", "test-key", false)
51+
client := NewClient("test-key", WithBaseURL("https://example.com/api/v1"))
5252

5353
tests := []struct {
5454
name string
@@ -88,7 +88,7 @@ func TestNewRequest(t *testing.T) {
8888
}
8989

9090
func TestWithUserAgent(t *testing.T) {
91-
client := NewClient("https://example.com/api/v1", "test-key", false).WithUserAgent("loops-cli/1.2.3")
91+
client := NewClient("test-key", WithBaseURL("https://example.com/api/v1"), WithUserAgent("loops-cli/1.2.3"))
9292
req, err := client.newRequest(http.MethodGet, "/api-key", nil)
9393
if err != nil {
9494
t.Fatalf("unexpected error: %v", err)
@@ -99,7 +99,7 @@ func TestWithUserAgent(t *testing.T) {
9999
}
100100

101101
func TestNewRequest_InvalidURL(t *testing.T) {
102-
client := NewClient("://bad-url", "test-key", false)
102+
client := NewClient("test-key", WithBaseURL("://bad-url"))
103103
_, err := client.newRequest(http.MethodGet, "/path", nil)
104104
if err == nil {
105105
t.Error("expected error for invalid URL, got nil")
@@ -221,7 +221,7 @@ func TestDo_Retries(t *testing.T) {
221221
}))
222222
defer server.Close()
223223

224-
client := NewClient(server.URL, "test-key", false)
224+
client := NewClient("test-key", WithBaseURL(server.URL))
225225
req, _ := client.newRequest(http.MethodGet, "/", nil)
226226
resp, err := client.do(req)
227227
if err != nil {

internal/api/contact_properties_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func TestListContactProperties(t *testing.T) {
6868
}))
6969
defer server.Close()
7070

71-
client := NewClient(server.URL, "test-key", false)
71+
client := NewClient("test-key", WithBaseURL(server.URL))
7272
props, err := client.ListContactProperties(tt.customOnly)
7373

7474
if tt.wantQuery != "" && gotQuery != tt.wantQuery {
@@ -147,7 +147,7 @@ func TestCreateContactProperty(t *testing.T) {
147147
}))
148148
defer server.Close()
149149

150-
client := NewClient(server.URL, "test-key", false)
150+
client := NewClient("test-key", WithBaseURL(server.URL))
151151
err := client.CreateContactProperty("age", "number")
152152

153153
if tt.wantBody != nil {
@@ -186,7 +186,7 @@ func TestListContactProperties_ResponseData(t *testing.T) {
186186
}))
187187
defer server.Close()
188188

189-
client := NewClient(server.URL, "test-key", false)
189+
client := NewClient("test-key", WithBaseURL(server.URL))
190190
props, err := client.ListContactProperties(false)
191191
if err != nil {
192192
t.Fatalf("unexpected error: %v", err)

internal/api/contacts_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func TestCreateContact(t *testing.T) {
115115
}))
116116
defer server.Close()
117117

118-
client := NewClient(server.URL, "test-key", false)
118+
client := NewClient("test-key", WithBaseURL(server.URL))
119119
id, err := client.CreateContact(tt.req)
120120

121121
if tt.wantBody != nil {
@@ -210,7 +210,7 @@ func TestDeleteContact(t *testing.T) {
210210
}))
211211
defer server.Close()
212212

213-
client := NewClient(server.URL, "test-key", false)
213+
client := NewClient("test-key", WithBaseURL(server.URL))
214214
err := client.DeleteContact(tt.email, tt.userID)
215215

216216
if tt.wantBody != nil {
@@ -323,7 +323,7 @@ func TestUpdateContact(t *testing.T) {
323323
}))
324324
defer server.Close()
325325

326-
client := NewClient(server.URL, "test-key", false)
326+
client := NewClient("test-key", WithBaseURL(server.URL))
327327
err := client.UpdateContact(tt.req)
328328

329329
if tt.wantBody != nil {
@@ -433,7 +433,7 @@ func TestCheckContactSuppression(t *testing.T) {
433433
}))
434434
defer server.Close()
435435

436-
client := NewClient(server.URL, "test-key", false)
436+
client := NewClient("test-key", WithBaseURL(server.URL))
437437
result, err := client.CheckContactSuppression(tt.email, tt.userID)
438438

439439
if tt.wantQuery != "" && gotQuery != tt.wantQuery {
@@ -549,7 +549,7 @@ func TestRemoveContactSuppression(t *testing.T) {
549549
}))
550550
defer server.Close()
551551

552-
client := NewClient(server.URL, "test-key", false)
552+
client := NewClient("test-key", WithBaseURL(server.URL))
553553
result, err := client.RemoveContactSuppression(tt.email, tt.userID)
554554

555555
if tt.wantQuery != "" && gotQuery != tt.wantQuery {
@@ -650,7 +650,7 @@ func TestFindContacts(t *testing.T) {
650650
}))
651651
defer server.Close()
652652

653-
client := NewClient(server.URL, "test-key", false)
653+
client := NewClient("test-key", WithBaseURL(server.URL))
654654
contacts, err := client.FindContacts(tt.params)
655655

656656
if tt.wantQuery != "" && gotQuery != tt.wantQuery {

internal/api/email_messages_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func TestGetEmailMessage(t *testing.T) {
7474
}))
7575
defer server.Close()
7676

77-
client := NewClient(server.URL, "test-key", false)
77+
client := NewClient("test-key", WithBaseURL(server.URL))
7878
result, err := client.GetEmailMessage(tt.id)
7979

8080
if tt.wantAPIErr != nil {
@@ -186,7 +186,7 @@ func TestUpdateEmailMessage(t *testing.T) {
186186
}))
187187
defer server.Close()
188188

189-
client := NewClient(server.URL, "test-key", false)
189+
client := NewClient("test-key", WithBaseURL(server.URL))
190190
req := UpdateEmailMessageRequest{
191191
EmailMessageFields: EmailMessageFields{Subject: "Updated"},
192192
Set: map[string]bool{"subject": true},
@@ -296,7 +296,7 @@ func TestUpdateEmailMessage_RequestBody(t *testing.T) {
296296
}))
297297
defer server.Close()
298298

299-
client := NewClient(server.URL, "test-key", false)
299+
client := NewClient("test-key", WithBaseURL(server.URL))
300300
if _, err := client.UpdateEmailMessage("em_abc123", tt.req); err != nil {
301301
t.Fatalf("unexpected error: %v", err)
302302
}

internal/api/events_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestSendEvent(t *testing.T) {
4949
}))
5050
defer server.Close()
5151

52-
client := NewClient(server.URL, "test-key", false)
52+
client := NewClient("test-key", WithBaseURL(server.URL))
5353
err := client.SendEvent(SendEventRequest{
5454
Email: "test@example.com",
5555
EventName: "signup",
@@ -176,7 +176,7 @@ func TestSendEvent_RequestBody(t *testing.T) {
176176
}))
177177
defer server.Close()
178178

179-
client := NewClient(server.URL, "test-key", false)
179+
client := NewClient("test-key", WithBaseURL(server.URL))
180180
client.SendEvent(tt.req)
181181

182182
for key, want := range tt.wantPresent {
@@ -258,7 +258,7 @@ func TestSendEvent_IdempotencyKey(t *testing.T) {
258258
}))
259259
defer server.Close()
260260

261-
client := NewClient(server.URL, "test-key", false)
261+
client := NewClient("test-key", WithBaseURL(server.URL))
262262
client.SendEvent(SendEventRequest{
263263
Email: "a@b.com",
264264
EventName: "click",

0 commit comments

Comments
 (0)