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