Skip to content
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
10 changes: 8 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ var teamFlag string
var debugFlag bool

func newAPIClient(cfg *config.Config) *api.Client {
return api.NewClient(cfg.EndpointURL, cfg.APIKey, cfg.Debug).
WithUserAgent("loops-cli/" + version)
opts := []api.Option{
api.WithBaseURL(cfg.EndpointURL),
api.WithUserAgent("loops-cli/" + version),
}
if cfg.Debug {
opts = append(opts, api.WithLogger(os.Stderr))
}
return api.NewClient(cfg.APIKey, opts...)
}

func loadConfig() (*config.Config, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/api_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestGetAPIKey(t *testing.T) {
}))
defer server.Close()

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

if tt.wantAPIErr != nil {
Expand Down
16 changes: 8 additions & 8 deletions internal/api/campaigns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func TestCreateCampaign(t *testing.T) {
}))
defer server.Close()

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

if tt.wantAPIErr != nil {
Expand Down Expand Up @@ -141,7 +141,7 @@ func TestCreateCampaign_RequestBody(t *testing.T) {
}))
defer server.Close()

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

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

if tt.wantAPIErr != nil {
Expand Down Expand Up @@ -261,7 +261,7 @@ func TestUpdateCampaign_RequestBodyAndPath(t *testing.T) {
}))
defer server.Close()

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

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

if tt.wantAPIErr != nil {
Expand Down Expand Up @@ -429,7 +429,7 @@ func TestListCampaigns(t *testing.T) {
}))
defer server.Close()

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

if tt.wantAPIErr != nil {
Expand Down Expand Up @@ -476,7 +476,7 @@ func TestListCampaigns_ResponseData(t *testing.T) {
}))
defer server.Close()

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

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

if gotPerPage != tt.wantPerPage {
Expand Down
53 changes: 29 additions & 24 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import (
"io"
"math/rand/v2"
"net/http"
"os"
"time"
)

const (
maxRetries = 2
baseDelay = 500 * time.Millisecond
DefaultBaseURL = "https://app.loops.so/api/v1"
maxRetries = 2
baseDelay = 500 * time.Millisecond
)

var sleep = time.Sleep
Expand All @@ -35,22 +35,27 @@ type Client struct {
baseURL string
apiKey string
httpClient *http.Client
debug bool
logger io.Writer
userAgent string
}

func NewClient(baseURL, apiKey string, debug bool) *Client {
return &Client{
baseURL: baseURL,
type Option func(*Client)

func WithBaseURL(u string) Option { return func(c *Client) { c.baseURL = u } }
func WithUserAgent(ua string) Option { return func(c *Client) { c.userAgent = ua } }
func WithLogger(w io.Writer) Option { return func(c *Client) { c.logger = w } }
func WithHTTPClient(h *http.Client) Option { return func(c *Client) { c.httpClient = h } }

func NewClient(apiKey string, opts ...Option) *Client {
c := &Client{
baseURL: DefaultBaseURL,
apiKey: apiKey,
httpClient: &http.Client{Timeout: 5 * time.Second},
debug: debug,
userAgent: "loops-go/dev",
}
}

func (c *Client) WithUserAgent(ua string) *Client {
c.userAgent = ua
for _, opt := range opts {
opt(c)
}
return c
}

Expand All @@ -73,20 +78,20 @@ func errorFromResponse(resp *http.Response) *APIError {
func (c *Client) logResponse(resp *http.Response) {
raw, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "[debug] Response: %s (body read failed: %v)\n", resp.Status, err)
fmt.Fprintf(c.logger, "[debug] Response: %s (body read failed: %v)\n", resp.Status, err)
resp.Body = io.NopCloser(bytes.NewReader(nil))
return
}
resp.Body = io.NopCloser(bytes.NewReader(raw))
fmt.Fprintf(os.Stderr, "[debug] Response: %s (%d bytes)\n", resp.Status, len(raw))
fmt.Fprintf(c.logger, "[debug] Response: %s (%d bytes)\n", resp.Status, len(raw))
if len(raw) == 0 {
return
}
var pretty bytes.Buffer
if json.Indent(&pretty, raw, "", " ") == nil {
fmt.Fprintf(os.Stderr, "[debug] Body:\n%s\n", pretty.String())
fmt.Fprintf(c.logger, "[debug] Body:\n%s\n", pretty.String())
} else {
fmt.Fprintf(os.Stderr, "[debug] Body: %s\n", raw)
fmt.Fprintf(c.logger, "[debug] Body: %s\n", raw)
}
}

Expand Down Expand Up @@ -116,7 +121,7 @@ func (c *Client) do(req *http.Request) (*http.Response, error) {
continue
}
if !isRetryable(resp.StatusCode) {
if c.debug {
if c.logger != nil {
c.logResponse(resp)
}
return resp, nil
Expand All @@ -135,7 +140,7 @@ func (c *Client) newRequest(method, path string, body io.Reader) (*http.Request,
url := fmt.Sprintf("%s%s", c.baseURL, path)

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

if c.debug {
fmt.Fprintf(os.Stderr, "[debug] %s %s\n", method, url)
fmt.Fprintf(os.Stderr, "[debug] Authorization: Bearer [REDACTED]\n")
if c.logger != nil {
fmt.Fprintf(c.logger, "[debug] %s %s\n", method, url)
fmt.Fprintf(c.logger, "[debug] Authorization: Bearer [REDACTED]\n")
if req.Header.Get("Content-Type") != "" {
fmt.Fprintf(os.Stderr, "[debug] Content-Type: %s\n", req.Header.Get("Content-Type"))
fmt.Fprintf(c.logger, "[debug] Content-Type: %s\n", req.Header.Get("Content-Type"))
}
if len(bodyBytes) > 0 {
var pretty bytes.Buffer
if json.Indent(&pretty, bodyBytes, "", " ") == nil {
fmt.Fprintf(os.Stderr, "[debug] Body:\n%s\n", pretty.String())
fmt.Fprintf(c.logger, "[debug] Body:\n%s\n", pretty.String())
} else {
fmt.Fprintf(os.Stderr, "[debug] Body: %s\n", bodyBytes)
fmt.Fprintf(c.logger, "[debug] Body: %s\n", bodyBytes)
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions internal/api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestDo_RetryResetsBody(t *testing.T) {
}))
defer server.Close()

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

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

tests := []struct {
name string
Expand Down Expand Up @@ -88,7 +88,7 @@ func TestNewRequest(t *testing.T) {
}

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

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

client := NewClient(server.URL, "test-key", false)
client := NewClient("test-key", WithBaseURL(server.URL))
req, _ := client.newRequest(http.MethodGet, "/", nil)
resp, err := client.do(req)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions internal/api/contact_properties_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestListContactProperties(t *testing.T) {
}))
defer server.Close()

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

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

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

if tt.wantBody != nil {
Expand Down Expand Up @@ -186,7 +186,7 @@ func TestListContactProperties_ResponseData(t *testing.T) {
}))
defer server.Close()

client := NewClient(server.URL, "test-key", false)
client := NewClient("test-key", WithBaseURL(server.URL))
props, err := client.ListContactProperties(false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
Expand Down
12 changes: 6 additions & 6 deletions internal/api/contacts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func TestCreateContact(t *testing.T) {
}))
defer server.Close()

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

if tt.wantBody != nil {
Expand Down Expand Up @@ -210,7 +210,7 @@ func TestDeleteContact(t *testing.T) {
}))
defer server.Close()

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

if tt.wantBody != nil {
Expand Down Expand Up @@ -323,7 +323,7 @@ func TestUpdateContact(t *testing.T) {
}))
defer server.Close()

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

if tt.wantBody != nil {
Expand Down Expand Up @@ -433,7 +433,7 @@ func TestCheckContactSuppression(t *testing.T) {
}))
defer server.Close()

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

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

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

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

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

if tt.wantQuery != "" && gotQuery != tt.wantQuery {
Expand Down
6 changes: 3 additions & 3 deletions internal/api/email_messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestGetEmailMessage(t *testing.T) {
}))
defer server.Close()

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

if tt.wantAPIErr != nil {
Expand Down Expand Up @@ -186,7 +186,7 @@ func TestUpdateEmailMessage(t *testing.T) {
}))
defer server.Close()

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

client := NewClient(server.URL, "test-key", false)
client := NewClient("test-key", WithBaseURL(server.URL))
if _, err := client.UpdateEmailMessage("em_abc123", tt.req); err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/api/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestSendEvent(t *testing.T) {
}))
defer server.Close()

client := NewClient(server.URL, "test-key", false)
client := NewClient("test-key", WithBaseURL(server.URL))
err := client.SendEvent(SendEventRequest{
Email: "test@example.com",
EventName: "signup",
Expand Down Expand Up @@ -176,7 +176,7 @@ func TestSendEvent_RequestBody(t *testing.T) {
}))
defer server.Close()

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

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

client := NewClient(server.URL, "test-key", false)
client := NewClient("test-key", WithBaseURL(server.URL))
client.SendEvent(SendEventRequest{
Email: "a@b.com",
EventName: "click",
Expand Down
Loading