request_helpers.go raw
1 package linodego
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/url"
8 "reflect"
9 )
10
11 // paginatedResponse represents a single response from a paginated
12 // endpoint.
13 type paginatedResponse[T any] struct {
14 Page int `json:"page" url:"page,omitempty"`
15 Pages int `json:"pages" url:"pages,omitempty"`
16 Results int `json:"results" url:"results,omitempty"`
17 Data []T `json:"data"`
18 }
19
20 // handlePaginatedResults aggregates results from the given
21 // paginated endpoint using the provided ListOptions and HTTP method.
22 // nolint:funlen
23 func handlePaginatedResults[T any, O any](
24 ctx context.Context,
25 client *Client,
26 endpoint string,
27 opts *ListOptions,
28 method string,
29 options ...O,
30 ) ([]T, error) {
31 result := make([]T, 0)
32
33 if opts == nil {
34 opts = &ListOptions{PageOptions: &PageOptions{Page: 0}}
35 }
36
37 if opts.PageOptions == nil {
38 opts.PageOptions = &PageOptions{Page: 0}
39 }
40
41 // Validate options
42 numOpts := len(options)
43 if numOpts > 1 {
44 return nil, fmt.Errorf("invalid number of options: expected 0 or 1, got %d", numOpts)
45 }
46
47 // Prepare request body if options are provided
48 var reqBody string
49
50 if numOpts > 0 && !isNil(options[0]) {
51 body, err := json.Marshal(options[0])
52 if err != nil {
53 return nil, fmt.Errorf("failed to marshal request body: %w", err)
54 }
55
56 reqBody = string(body)
57 }
58
59 // Makes a request to a particular page and appends the response to the result
60 handlePage := func(page int) error {
61 var resultType paginatedResponse[T]
62
63 // Override the page to be applied in applyListOptionsToRequest(...)
64 opts.Page = page
65
66 // This request object cannot be reused for each page request
67 // because it can lead to possible data corruption
68 req := client.R(ctx).SetResult(&resultType)
69
70 // Apply all user-provided list options to the request
71 if err := applyListOptionsToRequest(opts, req); err != nil {
72 return err
73 }
74
75 // Set request body if provided
76 if reqBody != "" {
77 req.SetBody(reqBody)
78 }
79
80 var response *paginatedResponse[T]
81 // Execute the appropriate HTTP method
82 switch method {
83 case "GET":
84 res, err := coupleAPIErrors(req.Get(endpoint))
85 if err != nil {
86 return err
87 }
88
89 response = res.Result().(*paginatedResponse[T])
90 case "PUT":
91 res, err := coupleAPIErrors(req.Put(endpoint))
92 if err != nil {
93 return err
94 }
95
96 response = res.Result().(*paginatedResponse[T])
97 case "POST":
98 res, err := coupleAPIErrors(req.Post(endpoint))
99 if err != nil {
100 return err
101 }
102
103 response = res.Result().(*paginatedResponse[T])
104 default:
105 return fmt.Errorf("unsupported HTTP method: %s", method)
106 }
107
108 // Update pagination metadata
109 opts.Page = page
110 opts.Pages = response.Pages
111 opts.Results = response.Results
112 result = append(result, response.Data...)
113
114 return nil
115 }
116
117 // Determine starting page
118 startingPage := 1
119 pageDefined := opts.Page > 0
120
121 if pageDefined {
122 startingPage = opts.Page
123 }
124
125 // Get the first page
126 if err := handlePage(startingPage); err != nil {
127 return nil, err
128 }
129
130 // If the user has explicitly specified a page, we don't
131 // need to get any other pages.
132 if pageDefined {
133 return result, nil
134 }
135
136 // Get the rest of the pages
137 for page := 2; page <= opts.Pages; page++ {
138 if err := handlePage(page); err != nil {
139 return nil, err
140 }
141 }
142
143 return result, nil
144 }
145
146 // getPaginatedResults aggregates results from the given
147 // paginated endpoint using the provided ListOptions.
148 func getPaginatedResults[T any](
149 ctx context.Context,
150 client *Client,
151 endpoint string,
152 opts *ListOptions,
153 ) ([]T, error) {
154 return handlePaginatedResults[T, any](ctx, client, endpoint, opts, "GET")
155 }
156
157 // putPaginatedResults sends a PUT request and aggregates the results from the given
158 // paginated endpoint using the provided ListOptions.
159 func putPaginatedResults[T, O any](
160 ctx context.Context,
161 client *Client,
162 endpoint string,
163 opts *ListOptions,
164 options ...O,
165 ) ([]T, error) {
166 return handlePaginatedResults[T, O](ctx, client, endpoint, opts, "PUT", options...)
167 }
168
169 // postPaginatedResults sends a POST request and aggregates the results from the given
170 // paginated endpoint using the provided ListOptions.
171 func postPaginatedResults[T, O any](
172 ctx context.Context,
173 client *Client,
174 endpoint string,
175 opts *ListOptions,
176 options ...O,
177 ) ([]T, error) {
178 return handlePaginatedResults[T, O](ctx, client, endpoint, opts, "POST", options...)
179 }
180
181 // doGETRequest runs a GET request using the given client and API endpoint,
182 // and returns the result
183 func doGETRequest[T any](
184 ctx context.Context,
185 client *Client,
186 endpoint string,
187 ) (*T, error) {
188 var resultType T
189
190 req := client.R(ctx).SetResult(&resultType)
191
192 r, err := coupleAPIErrors(req.Get(endpoint))
193 if err != nil {
194 return nil, err
195 }
196
197 return r.Result().(*T), nil
198 }
199
200 // doPOSTRequest runs a PUT request using the given client, API endpoint,
201 // and options/body.
202 func doPOSTRequest[T, O any](
203 ctx context.Context,
204 client *Client,
205 endpoint string,
206 options ...O,
207 ) (*T, error) {
208 var resultType T
209
210 numOpts := len(options)
211
212 if numOpts > 1 {
213 return nil, fmt.Errorf("invalid number of options: %d", len(options))
214 }
215
216 req := client.R(ctx).SetResult(&resultType)
217
218 if numOpts > 0 && !isNil(options[0]) {
219 body, err := json.Marshal(options[0])
220 if err != nil {
221 return nil, err
222 }
223
224 req.SetBody(string(body))
225 }
226
227 r, err := coupleAPIErrors(req.Post(endpoint))
228 if err != nil {
229 return nil, err
230 }
231
232 return r.Result().(*T), nil
233 }
234
235 // doPOSTRequestNoResponseBody runs a POST request using the given client, API endpoint,
236 // and options/body. It expects only empty response from the endpoint.
237 func doPOSTRequestNoResponseBody[T any](
238 ctx context.Context,
239 client *Client,
240 endpoint string,
241 options ...T,
242 ) error {
243 _, err := doPOSTRequest[any, T](ctx, client, endpoint, options...)
244 return err
245 }
246
247 // doPOSTRequestNoRequestResponseBody runs a POST request where no request body is needed and no response body
248 // is expected from the endpoints.
249 func doPOSTRequestNoRequestResponseBody(
250 ctx context.Context,
251 client *Client,
252 endpoint string,
253 ) error {
254 return doPOSTRequestNoResponseBody(ctx, client, endpoint, struct{}{})
255 }
256
257 // doPUTRequest runs a PUT request using the given client, API endpoint,
258 // and options/body.
259 func doPUTRequest[T, O any](
260 ctx context.Context,
261 client *Client,
262 endpoint string,
263 options ...O,
264 ) (*T, error) {
265 var resultType T
266
267 numOpts := len(options)
268
269 if numOpts > 1 {
270 return nil, fmt.Errorf("invalid number of options: %d", len(options))
271 }
272
273 req := client.R(ctx).SetResult(&resultType)
274
275 if numOpts > 0 && !isNil(options[0]) {
276 body, err := json.Marshal(options[0])
277 if err != nil {
278 return nil, err
279 }
280
281 req.SetBody(string(body))
282 }
283
284 r, err := coupleAPIErrors(req.Put(endpoint))
285 if err != nil {
286 return nil, err
287 }
288
289 return r.Result().(*T), nil
290 }
291
292 // doDELETERequest runs a DELETE request using the given client
293 // and API endpoint.
294 func doDELETERequest(
295 ctx context.Context,
296 client *Client,
297 endpoint string,
298 ) error {
299 req := client.R(ctx)
300 _, err := coupleAPIErrors(req.Delete(endpoint))
301
302 return err
303 }
304
305 // formatAPIPath allows us to safely build an API request with path escaping
306 func formatAPIPath(format string, args ...any) string {
307 escapedArgs := make([]any, len(args))
308 for i, arg := range args {
309 if typeStr, ok := arg.(string); ok {
310 arg = url.PathEscape(typeStr)
311 }
312
313 escapedArgs[i] = arg
314 }
315
316 return fmt.Sprintf(format, escapedArgs...)
317 }
318
319 func isNil(i any) bool {
320 if i == nil {
321 return true
322 }
323
324 // Check for nil pointers
325 v := reflect.ValueOf(i)
326
327 return v.Kind() == reflect.Ptr && v.IsNil()
328 }
329