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