-
Notifications
You must be signed in to change notification settings - Fork 82
/
pagination.go
168 lines (134 loc) · 3.91 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
package linodego
/**
* Pagination and Filtering types and helpers
*/
import (
"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)
}
// 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 := range valType.NumField() {
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())
}
}
type legacyPagedResponse[T any] struct {
*PageOptions
Data []T `json:"data"`
}