Skip to content

Commit b4d0c8c

Browse files
committed
extract from cli
1 parent 115be88 commit b4d0c8c

28 files changed

Lines changed: 4242 additions & 0 deletions

.github/workflows/release.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v6
16+
- uses: actions/setup-go@v6
17+
with:
18+
go-version-file: go.mod
19+
- env:
20+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21+
run: gh release create "$GITHUB_REF_NAME" --generate-notes

.github/workflows/test.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: test
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
workflow_dispatch:
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v6
14+
- uses: actions/setup-go@v6
15+
with:
16+
go-version-file: go.mod
17+
- uses: go-task/setup-task@v2
18+
- run: task test

.github/workflows/vuln.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: vuln
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
7+
jobs:
8+
vuln:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v6
12+
- uses: actions/setup-go@v6
13+
with:
14+
go-version-file: go.mod
15+
- uses: go-task/setup-task@v2
16+
- run: task vuln

Taskfile.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# https://taskfile.dev
2+
3+
version: "3"
4+
5+
tasks:
6+
deps:
7+
desc: install/update deps
8+
cmds:
9+
- go mod tidy
10+
11+
test:
12+
desc: run tests
13+
aliases: [t]
14+
cmds:
15+
- go test ./... -cover
16+
17+
vuln:
18+
desc: run govulncheck
19+
aliases: [v]
20+
cmds:
21+
- go tool govulncheck {{.CLI_ARGS}} ./...
22+
23+
release:
24+
desc: tag a new sdk version
25+
requires:
26+
vars:
27+
- tag
28+
cmds:
29+
- git pull --rebase
30+
- git tag {{.tag}}
31+
- git push origin {{.tag}}

api_key.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package loops
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
)
8+
9+
type APIKeyResponse struct {
10+
TeamName string `json:"teamName"`
11+
}
12+
13+
func (c *Client) GetAPIKey() (*APIKeyResponse, error) {
14+
req, err := c.newRequest(http.MethodGet, "/api-key", nil)
15+
if err != nil {
16+
return nil, err
17+
}
18+
19+
resp, err := c.do(req)
20+
if err != nil {
21+
return nil, err
22+
}
23+
defer resp.Body.Close()
24+
25+
if resp.StatusCode != http.StatusOK {
26+
return nil, errorFromResponse(resp)
27+
}
28+
29+
var result APIKeyResponse
30+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
31+
return nil, fmt.Errorf("failed to decode response: %w", err)
32+
}
33+
34+
return &result, nil
35+
}

api_key_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package loops
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"net/http/httptest"
7+
"strings"
8+
"testing"
9+
)
10+
11+
func TestGetAPIKey(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
statusCode int
15+
body string
16+
wantAPIErr *APIError
17+
wantErrMsg string
18+
wantTeam string
19+
}{
20+
{
21+
name: "success",
22+
statusCode: http.StatusOK,
23+
body: `{"teamName":"Acme"}`,
24+
wantTeam: "Acme",
25+
},
26+
{
27+
name: "unauthorized",
28+
statusCode: http.StatusUnauthorized,
29+
body: `{"success":false,"error":"Invalid API key"}`,
30+
wantAPIErr: &APIError{StatusCode: http.StatusUnauthorized, Message: "Invalid API key"},
31+
},
32+
{
33+
name: "unexpected status",
34+
statusCode: http.StatusInternalServerError,
35+
body: ``,
36+
wantAPIErr: &APIError{StatusCode: http.StatusInternalServerError},
37+
},
38+
{
39+
name: "invalid json",
40+
statusCode: http.StatusOK,
41+
body: `not json`,
42+
wantErrMsg: "failed to decode response",
43+
},
44+
}
45+
46+
for _, tt := range tests {
47+
t.Run(tt.name, func(t *testing.T) {
48+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49+
w.WriteHeader(tt.statusCode)
50+
w.Write([]byte(tt.body))
51+
}))
52+
defer server.Close()
53+
54+
client := NewClient("test-key", WithBaseURL(server.URL))
55+
result, err := client.GetAPIKey()
56+
57+
if tt.wantAPIErr != nil {
58+
var apiErr *APIError
59+
if !errors.As(err, &apiErr) {
60+
t.Fatalf("expected *APIError, got %T: %v", err, err)
61+
}
62+
if apiErr.StatusCode != tt.wantAPIErr.StatusCode {
63+
t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, tt.wantAPIErr.StatusCode)
64+
}
65+
if tt.wantAPIErr.Message != "" && apiErr.Message != tt.wantAPIErr.Message {
66+
t.Errorf("Message = %q, want %q", apiErr.Message, tt.wantAPIErr.Message)
67+
}
68+
return
69+
}
70+
71+
if tt.wantErrMsg != "" {
72+
if err == nil {
73+
t.Fatalf("expected error containing %q, got nil", tt.wantErrMsg)
74+
}
75+
if !strings.Contains(err.Error(), tt.wantErrMsg) {
76+
t.Errorf("error = %q, want it to contain %q", err.Error(), tt.wantErrMsg)
77+
}
78+
return
79+
}
80+
81+
if err != nil {
82+
t.Fatalf("unexpected error: %v", err)
83+
}
84+
if result.TeamName != tt.wantTeam {
85+
t.Errorf("TeamName = %q, want %q", result.TeamName, tt.wantTeam)
86+
}
87+
})
88+
}
89+
}

campaigns.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package loops
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
)
10+
11+
type Campaign struct {
12+
CampaignID string `json:"campaignId"`
13+
EmailMessageID *string `json:"emailMessageId"`
14+
Name string `json:"name"`
15+
Status string `json:"status"`
16+
CreatedAt string `json:"createdAt"`
17+
UpdatedAt string `json:"updatedAt"`
18+
}
19+
20+
type CampaignListItem struct {
21+
CampaignID string `json:"campaignId"`
22+
EmailMessageID *string `json:"emailMessageId"`
23+
Name string `json:"name"`
24+
Subject string `json:"subject"`
25+
Status string `json:"status"`
26+
CreatedAt string `json:"createdAt"`
27+
UpdatedAt string `json:"updatedAt"`
28+
}
29+
30+
type LmxWarning struct {
31+
Rule string `json:"rule"`
32+
Severity string `json:"severity"`
33+
Message string `json:"message"`
34+
Path string `json:"path,omitempty"`
35+
}
36+
37+
type EmailMessageFields struct {
38+
Subject string `json:"subject,omitempty"`
39+
PreviewText string `json:"previewText,omitempty"`
40+
FromName string `json:"fromName,omitempty"`
41+
FromEmail string `json:"fromEmail,omitempty"`
42+
ReplyToEmail string `json:"replyToEmail,omitempty"`
43+
LMX string `json:"lmx,omitempty"`
44+
}
45+
46+
type CreateCampaignRequest struct {
47+
Name string `json:"name"`
48+
}
49+
50+
type UpdateCampaignRequest struct {
51+
Name string `json:"name"`
52+
}
53+
54+
type CampaignCreateResponse struct {
55+
Campaign
56+
EmailMessageContentRevisionID *string `json:"emailMessageContentRevisionId"`
57+
}
58+
59+
func (c *Client) CreateCampaign(req CreateCampaignRequest) (*CampaignCreateResponse, error) {
60+
b, err := json.Marshal(req)
61+
if err != nil {
62+
return nil, fmt.Errorf("failed to encode request: %w", err)
63+
}
64+
65+
httpReq, err := c.newRequest(http.MethodPost, "/campaigns", bytes.NewReader(b))
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
resp, err := c.do(httpReq)
71+
if err != nil {
72+
return nil, err
73+
}
74+
defer resp.Body.Close()
75+
76+
if resp.StatusCode != http.StatusCreated {
77+
return nil, errorFromResponse(resp)
78+
}
79+
80+
var result CampaignCreateResponse
81+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
82+
return nil, fmt.Errorf("failed to decode response: %w", err)
83+
}
84+
85+
return &result, nil
86+
}
87+
88+
func (c *Client) UpdateCampaign(id string, req UpdateCampaignRequest) (*Campaign, error) {
89+
b, err := json.Marshal(req)
90+
if err != nil {
91+
return nil, fmt.Errorf("failed to encode request: %w", err)
92+
}
93+
94+
httpReq, err := c.newRequest(http.MethodPost, "/campaigns/"+id, bytes.NewReader(b))
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
resp, err := c.do(httpReq)
100+
if err != nil {
101+
return nil, err
102+
}
103+
defer resp.Body.Close()
104+
105+
if resp.StatusCode != http.StatusOK {
106+
return nil, errorFromResponse(resp)
107+
}
108+
109+
var result Campaign
110+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
111+
return nil, fmt.Errorf("failed to decode response: %w", err)
112+
}
113+
114+
return &result, nil
115+
}
116+
117+
func (c *Client) GetCampaign(id string) (*Campaign, error) {
118+
req, err := c.newRequest(http.MethodGet, "/campaigns/"+id, nil)
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
resp, err := c.do(req)
124+
if err != nil {
125+
return nil, err
126+
}
127+
defer resp.Body.Close()
128+
129+
if resp.StatusCode != http.StatusOK {
130+
return nil, errorFromResponse(resp)
131+
}
132+
133+
var result Campaign
134+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
135+
return nil, fmt.Errorf("failed to decode response: %w", err)
136+
}
137+
138+
return &result, nil
139+
}
140+
141+
func (c *Client) ListCampaigns(params PaginationParams) ([]CampaignListItem, *Pagination, error) {
142+
q := url.Values{}
143+
if params.PerPage != "" {
144+
q.Set("perPage", params.PerPage)
145+
}
146+
if params.Cursor != "" {
147+
q.Set("cursor", params.Cursor)
148+
}
149+
150+
path := "/campaigns"
151+
if len(q) > 0 {
152+
path += "?" + q.Encode()
153+
}
154+
155+
req, err := c.newRequest(http.MethodGet, path, nil)
156+
if err != nil {
157+
return nil, nil, err
158+
}
159+
160+
resp, err := c.do(req)
161+
if err != nil {
162+
return nil, nil, err
163+
}
164+
defer resp.Body.Close()
165+
166+
if resp.StatusCode != http.StatusOK {
167+
return nil, nil, errorFromResponse(resp)
168+
}
169+
170+
var result struct {
171+
Pagination Pagination `json:"pagination"`
172+
Data []CampaignListItem `json:"data"`
173+
}
174+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
175+
return nil, nil, fmt.Errorf("failed to decode response: %w", err)
176+
}
177+
178+
return result.Data, &result.Pagination, nil
179+
}

0 commit comments

Comments
 (0)