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"`
}
|