pagination.go raw

   1  package linodego
   2  
   3  /**
   4   * Pagination and Filtering types and helpers
   5   */
   6  
   7  import (
   8  	"crypto/sha256"
   9  	"encoding/hex"
  10  	"encoding/json"
  11  	"fmt"
  12  	"reflect"
  13  	"strconv"
  14  
  15  	"github.com/go-resty/resty/v2"
  16  )
  17  
  18  // PageOptions are the pagination parameters for List endpoints
  19  type PageOptions struct {
  20  	Page    int `json:"page"    url:"page,omitempty"`
  21  	Pages   int `json:"pages"   url:"pages,omitempty"`
  22  	Results int `json:"results" url:"results,omitempty"`
  23  }
  24  
  25  // ListOptions are the pagination and filtering (TODO) parameters for endpoints
  26  // nolint
  27  type ListOptions struct {
  28  	*PageOptions
  29  	PageSize int    `json:"page_size"`
  30  	Filter   string `json:"filter"`
  31  
  32  	// QueryParams allows for specifying custom query parameters on list endpoint
  33  	// calls. QueryParams should be an instance of a struct containing fields with
  34  	// the `query` tag.
  35  	QueryParams any
  36  }
  37  
  38  // NewListOptions simplified construction of ListOptions using only
  39  // the two writable properties, Page and Filter
  40  func NewListOptions(page int, filter string) *ListOptions {
  41  	return &ListOptions{PageOptions: &PageOptions{Page: page}, Filter: filter}
  42  }
  43  
  44  // Hash returns the sha256 hash of the provided ListOptions.
  45  // This is necessary for caching purposes.
  46  func (l ListOptions) Hash() (string, error) {
  47  	data, err := json.Marshal(l)
  48  	if err != nil {
  49  		return "", fmt.Errorf("failed to cache ListOptions: %w", err)
  50  	}
  51  
  52  	h := sha256.New()
  53  
  54  	h.Write(data)
  55  
  56  	return hex.EncodeToString(h.Sum(nil)), nil
  57  }
  58  
  59  func applyListOptionsToRequest(opts *ListOptions, req *resty.Request) error {
  60  	if opts == nil {
  61  		return nil
  62  	}
  63  
  64  	if opts.QueryParams != nil {
  65  		params, err := flattenQueryStruct(opts.QueryParams)
  66  		if err != nil {
  67  			return fmt.Errorf("failed to apply list options: %w", err)
  68  		}
  69  
  70  		req.SetQueryParams(params)
  71  	}
  72  
  73  	if opts.PageOptions != nil && opts.Page > 0 {
  74  		req.SetQueryParam("page", strconv.Itoa(opts.Page))
  75  	}
  76  
  77  	if opts.PageSize > 0 {
  78  		req.SetQueryParam("page_size", strconv.Itoa(opts.PageSize))
  79  	}
  80  
  81  	if len(opts.Filter) > 0 {
  82  		req.SetHeader("X-Filter", opts.Filter)
  83  	}
  84  
  85  	return nil
  86  }
  87  
  88  type PagedResponse interface {
  89  	endpoint(...any) string
  90  	castResult(*resty.Request, string) (int, int, error)
  91  }
  92  
  93  // flattenQueryStruct flattens a structure into a Resty-compatible query param map.
  94  // Fields are mapped using the `query` struct tag.
  95  func flattenQueryStruct(val any) (map[string]string, error) {
  96  	result := make(map[string]string)
  97  
  98  	reflectVal := reflect.ValueOf(val)
  99  
 100  	// Deref pointer if necessary
 101  	if reflectVal.Kind() == reflect.Pointer {
 102  		if reflectVal.IsNil() {
 103  			return nil, fmt.Errorf("QueryParams is a nil pointer")
 104  		}
 105  
 106  		reflectVal = reflect.Indirect(reflectVal)
 107  	}
 108  
 109  	if reflectVal.Kind() != reflect.Struct {
 110  		return nil, fmt.Errorf(
 111  			"expected struct type for the QueryParams but got: %s",
 112  			reflectVal.Kind().String(),
 113  		)
 114  	}
 115  
 116  	valType := reflectVal.Type()
 117  
 118  	for i := range valType.NumField() {
 119  		currentField := valType.Field(i)
 120  
 121  		queryTag, ok := currentField.Tag.Lookup("query")
 122  		// Skip untagged fields
 123  		if !ok {
 124  			continue
 125  		}
 126  
 127  		valField := reflectVal.FieldByName(currentField.Name)
 128  		if !valField.IsValid() {
 129  			return nil, fmt.Errorf("invalid query param tag: %s", currentField.Name)
 130  		}
 131  
 132  		// Skip if it's a zero value
 133  		if valField.IsZero() {
 134  			continue
 135  		}
 136  
 137  		// Deref the pointer is necessary
 138  		if valField.Kind() == reflect.Pointer {
 139  			valField = reflect.Indirect(valField)
 140  		}
 141  
 142  		fieldString, err := queryFieldToString(valField)
 143  		if err != nil {
 144  			return nil, err
 145  		}
 146  
 147  		result[queryTag] = fieldString
 148  	}
 149  
 150  	return result, nil
 151  }
 152  
 153  func queryFieldToString(value reflect.Value) (string, error) {
 154  	switch value.Kind() {
 155  	case reflect.String:
 156  		return value.String(), nil
 157  	case reflect.Int64, reflect.Int32, reflect.Int:
 158  		return strconv.FormatInt(value.Int(), 10), nil
 159  	case reflect.Bool:
 160  		return strconv.FormatBool(value.Bool()), nil
 161  	default:
 162  		return "", fmt.Errorf("unsupported query param type: %s", value.Type().Name())
 163  	}
 164  }
 165  
 166  type legacyPagedResponse[T any] struct {
 167  	*PageOptions
 168  
 169  	Data []T `json:"data"`
 170  }
 171