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