Skip to content

Commit d612638

Browse files
authored
components api (#3)
1 parent 453715c commit d612638

3 files changed

Lines changed: 371 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ client := loops.NewClient("YOUR_API_KEY",
5858
- Transactional — `SendTransactional`, `ListTransactional`
5959
- Email messages — `GetEmailMessage`, `UpdateEmailMessage`
6060
- Campaigns — `CreateCampaign`, `UpdateCampaign`, `GetCampaign`, `ListCampaigns`
61+
- Components — `GetComponent`, `ListComponents`
6162

6263
Full reference: [pkg.go.dev/github.com/loops-so/loops-go](https://pkg.go.dev/github.com/loops-so/loops-go).
6364

components.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package loops
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"net/url"
8+
)
9+
10+
type Component struct {
11+
ComponentID string `json:"componentId"`
12+
Name string `json:"name"`
13+
LMX string `json:"lmx"`
14+
}
15+
16+
func (c *Client) GetComponent(id string) (*Component, error) {
17+
req, err := c.newRequest(http.MethodGet, "/components/"+id, nil)
18+
if err != nil {
19+
return nil, err
20+
}
21+
22+
resp, err := c.do(req)
23+
if err != nil {
24+
return nil, err
25+
}
26+
defer resp.Body.Close()
27+
28+
if resp.StatusCode != http.StatusOK {
29+
return nil, errorFromResponse(resp)
30+
}
31+
32+
var result Component
33+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
34+
return nil, fmt.Errorf("failed to decode response: %w", err)
35+
}
36+
37+
return &result, nil
38+
}
39+
40+
func (c *Client) ListComponents(params PaginationParams) ([]Component, *Pagination, error) {
41+
q := url.Values{}
42+
if params.PerPage != "" {
43+
q.Set("perPage", params.PerPage)
44+
}
45+
if params.Cursor != "" {
46+
q.Set("cursor", params.Cursor)
47+
}
48+
49+
path := "/components"
50+
if len(q) > 0 {
51+
path += "?" + q.Encode()
52+
}
53+
54+
req, err := c.newRequest(http.MethodGet, path, nil)
55+
if err != nil {
56+
return nil, nil, err
57+
}
58+
59+
resp, err := c.do(req)
60+
if err != nil {
61+
return nil, nil, err
62+
}
63+
defer resp.Body.Close()
64+
65+
if resp.StatusCode != http.StatusOK {
66+
return nil, nil, errorFromResponse(resp)
67+
}
68+
69+
var result struct {
70+
Pagination Pagination `json:"pagination"`
71+
Data []Component `json:"data"`
72+
}
73+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
74+
return nil, nil, fmt.Errorf("failed to decode response: %w", err)
75+
}
76+
77+
return result.Data, &result.Pagination, nil
78+
}

components_test.go

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
package loops
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"net/http/httptest"
7+
"strings"
8+
"testing"
9+
)
10+
11+
const getComponentResponse = `{
12+
"success": true,
13+
"componentId": "cmpt_abc123",
14+
"name": "Header",
15+
"lmx": "<H1>Hello</H1>"
16+
}`
17+
18+
const listComponentsResponse = `{
19+
"success": true,
20+
"pagination": {
21+
"totalResults": 2,
22+
"returnedResults": 2,
23+
"perPage": 20,
24+
"totalPages": 1,
25+
"nextCursor": "",
26+
"nextPage": ""
27+
},
28+
"data": [
29+
{
30+
"componentId": "cmpt_1",
31+
"name": "Header",
32+
"lmx": "<H1>Hello</H1>"
33+
},
34+
{
35+
"componentId": "cmpt_2",
36+
"name": "Footer",
37+
"lmx": "<Paragraph>Bye</Paragraph>"
38+
}
39+
]
40+
}`
41+
42+
func TestGetComponent(t *testing.T) {
43+
tests := []struct {
44+
name string
45+
id string
46+
statusCode int
47+
body string
48+
wantAPIErr *APIError
49+
wantErrMsg string
50+
wantID string
51+
}{
52+
{
53+
name: "success",
54+
id: "cmpt_abc123",
55+
statusCode: http.StatusOK,
56+
body: getComponentResponse,
57+
wantID: "cmpt_abc123",
58+
},
59+
{
60+
name: "not found",
61+
id: "cmpt_missing",
62+
statusCode: http.StatusNotFound,
63+
body: `{"success":false,"message":"Component not found"}`,
64+
wantAPIErr: &APIError{StatusCode: http.StatusNotFound, Message: "Component not found"},
65+
},
66+
{
67+
name: "invalid id",
68+
id: "bad",
69+
statusCode: http.StatusBadRequest,
70+
body: `{"success":false,"message":"Invalid componentId"}`,
71+
wantAPIErr: &APIError{StatusCode: http.StatusBadRequest, Message: "Invalid componentId"},
72+
},
73+
{
74+
name: "invalid json",
75+
id: "cmpt_abc123",
76+
statusCode: http.StatusOK,
77+
body: `not json`,
78+
wantErrMsg: "failed to decode response",
79+
},
80+
}
81+
82+
for _, tt := range tests {
83+
t.Run(tt.name, func(t *testing.T) {
84+
var gotPath string
85+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
86+
gotPath = r.URL.Path
87+
w.WriteHeader(tt.statusCode)
88+
w.Write([]byte(tt.body))
89+
}))
90+
defer server.Close()
91+
92+
client := NewClient("test-key", WithBaseURL(server.URL))
93+
result, err := client.GetComponent(tt.id)
94+
95+
if tt.wantAPIErr != nil {
96+
var apiErr *APIError
97+
if !errors.As(err, &apiErr) {
98+
t.Fatalf("expected *APIError, got %T: %v", err, err)
99+
}
100+
if apiErr.StatusCode != tt.wantAPIErr.StatusCode {
101+
t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, tt.wantAPIErr.StatusCode)
102+
}
103+
if apiErr.Message != tt.wantAPIErr.Message {
104+
t.Errorf("Message = %q, want %q", apiErr.Message, tt.wantAPIErr.Message)
105+
}
106+
return
107+
}
108+
109+
if tt.wantErrMsg != "" {
110+
if err == nil {
111+
t.Fatalf("expected error containing %q, got nil", tt.wantErrMsg)
112+
}
113+
if !strings.Contains(err.Error(), tt.wantErrMsg) {
114+
t.Errorf("error = %q, want it to contain %q", err.Error(), tt.wantErrMsg)
115+
}
116+
return
117+
}
118+
119+
if err != nil {
120+
t.Fatalf("unexpected error: %v", err)
121+
}
122+
if want := "/components/" + tt.id; gotPath != want {
123+
t.Errorf("path = %q, want %q", gotPath, want)
124+
}
125+
if result.ComponentID != tt.wantID {
126+
t.Errorf("ComponentID = %q, want %q", result.ComponentID, tt.wantID)
127+
}
128+
if result.Name != "Header" {
129+
t.Errorf("Name = %q, want Header", result.Name)
130+
}
131+
if result.LMX != "<H1>Hello</H1>" {
132+
t.Errorf("LMX = %q, want <H1>Hello</H1>", result.LMX)
133+
}
134+
})
135+
}
136+
}
137+
138+
func TestListComponents(t *testing.T) {
139+
tests := []struct {
140+
name string
141+
statusCode int
142+
body string
143+
wantAPIErr *APIError
144+
wantErrMsg string
145+
wantCount int
146+
}{
147+
{
148+
name: "success",
149+
statusCode: http.StatusOK,
150+
body: listComponentsResponse,
151+
wantCount: 2,
152+
},
153+
{
154+
name: "empty list",
155+
statusCode: http.StatusOK,
156+
body: `{"success":true,"pagination":{"totalResults":0},"data":[]}`,
157+
wantCount: 0,
158+
},
159+
{
160+
name: "unauthorized",
161+
statusCode: http.StatusUnauthorized,
162+
body: `{"success":false,"error":"Invalid API key"}`,
163+
wantAPIErr: &APIError{StatusCode: http.StatusUnauthorized, Message: "Invalid API key"},
164+
},
165+
{
166+
name: "invalid perPage",
167+
statusCode: http.StatusBadRequest,
168+
body: `{"success":false,"message":"perPage must be between 10 and 50"}`,
169+
wantAPIErr: &APIError{StatusCode: http.StatusBadRequest, Message: "perPage must be between 10 and 50"},
170+
},
171+
{
172+
name: "invalid json",
173+
statusCode: http.StatusOK,
174+
body: `not json`,
175+
wantErrMsg: "failed to decode response",
176+
},
177+
}
178+
179+
for _, tt := range tests {
180+
t.Run(tt.name, func(t *testing.T) {
181+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
182+
w.WriteHeader(tt.statusCode)
183+
w.Write([]byte(tt.body))
184+
}))
185+
defer server.Close()
186+
187+
client := NewClient("test-key", WithBaseURL(server.URL))
188+
components, pagination, err := client.ListComponents(PaginationParams{})
189+
190+
if tt.wantAPIErr != nil {
191+
var apiErr *APIError
192+
if !errors.As(err, &apiErr) {
193+
t.Fatalf("expected *APIError, got %T: %v", err, err)
194+
}
195+
if apiErr.StatusCode != tt.wantAPIErr.StatusCode {
196+
t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, tt.wantAPIErr.StatusCode)
197+
}
198+
if tt.wantAPIErr.Message != "" && apiErr.Message != tt.wantAPIErr.Message {
199+
t.Errorf("Message = %q, want %q", apiErr.Message, tt.wantAPIErr.Message)
200+
}
201+
return
202+
}
203+
204+
if tt.wantErrMsg != "" {
205+
if err == nil {
206+
t.Fatalf("expected error containing %q, got nil", tt.wantErrMsg)
207+
}
208+
if !strings.Contains(err.Error(), tt.wantErrMsg) {
209+
t.Errorf("error = %q, want it to contain %q", err.Error(), tt.wantErrMsg)
210+
}
211+
return
212+
}
213+
214+
if err != nil {
215+
t.Fatalf("unexpected error: %v", err)
216+
}
217+
if len(components) != tt.wantCount {
218+
t.Errorf("len(components) = %d, want %d", len(components), tt.wantCount)
219+
}
220+
if pagination == nil {
221+
t.Fatal("expected pagination, got nil")
222+
}
223+
})
224+
}
225+
}
226+
227+
func TestListComponents_ResponseData(t *testing.T) {
228+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
229+
w.WriteHeader(http.StatusOK)
230+
w.Write([]byte(listComponentsResponse))
231+
}))
232+
defer server.Close()
233+
234+
client := NewClient("test-key", WithBaseURL(server.URL))
235+
components, _, err := client.ListComponents(PaginationParams{})
236+
if err != nil {
237+
t.Fatalf("unexpected error: %v", err)
238+
}
239+
240+
if components[0].ComponentID != "cmpt_1" {
241+
t.Errorf("ComponentID = %q, want cmpt_1", components[0].ComponentID)
242+
}
243+
if components[0].Name != "Header" {
244+
t.Errorf("Name = %q, want Header", components[0].Name)
245+
}
246+
if components[1].LMX != "<Paragraph>Bye</Paragraph>" {
247+
t.Errorf("LMX = %q, want <Paragraph>Bye</Paragraph>", components[1].LMX)
248+
}
249+
}
250+
251+
func TestListComponents_QueryParams(t *testing.T) {
252+
tests := []struct {
253+
name string
254+
params PaginationParams
255+
wantPerPage string
256+
wantCursor string
257+
}{
258+
{
259+
name: "no params",
260+
params: PaginationParams{},
261+
},
262+
{
263+
name: "both params",
264+
params: PaginationParams{PerPage: "10", Cursor: "xyz"},
265+
wantPerPage: "10",
266+
wantCursor: "xyz",
267+
},
268+
}
269+
270+
for _, tt := range tests {
271+
t.Run(tt.name, func(t *testing.T) {
272+
var gotPerPage, gotCursor string
273+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
274+
gotPerPage = r.URL.Query().Get("perPage")
275+
gotCursor = r.URL.Query().Get("cursor")
276+
w.WriteHeader(http.StatusOK)
277+
w.Write([]byte(`{"pagination":{},"data":[]}`))
278+
}))
279+
defer server.Close()
280+
281+
client := NewClient("test-key", WithBaseURL(server.URL))
282+
client.ListComponents(tt.params)
283+
284+
if gotPerPage != tt.wantPerPage {
285+
t.Errorf("perPage = %q, want %q", gotPerPage, tt.wantPerPage)
286+
}
287+
if gotCursor != tt.wantCursor {
288+
t.Errorf("cursor = %q, want %q", gotCursor, tt.wantCursor)
289+
}
290+
})
291+
}
292+
}

0 commit comments

Comments
 (0)