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