156 lines
3.9 KiB
Go
156 lines
3.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
)
|
|
|
|
// ParseCursorInto parses an opaque base64 cursor and injects values into target struct.
|
|
//
|
|
// Supported sort types:
|
|
// - "id" → sets CursorID (must be *int64)
|
|
// - "year" → sets CursorID (*int64) + CursorYear (*int32)
|
|
// - "rating" → sets CursorID (*int64) + CursorRating (*float64)
|
|
//
|
|
// Target struct may have any subset of these fields (e.g. only CursorID).
|
|
// Unknown fields are ignored. Missing fields → values are dropped (safe).
|
|
//
|
|
// Returns error if cursor is invalid or inconsistent with sort_by.
|
|
func ParseCursorInto(sortBy, cursorStr string, target any) error {
|
|
if cursorStr == "" {
|
|
return nil // no cursor → nothing to do
|
|
}
|
|
|
|
// 1. Decode cursor
|
|
payload, err := decodeCursor(cursorStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2. Extract ID (required for all types)
|
|
id, err := extractInt64(payload, "id")
|
|
if err != nil {
|
|
return fmt.Errorf("cursor: %v", err)
|
|
}
|
|
|
|
// 3. Get reflect value of target (must be ptr to struct)
|
|
v := reflect.ValueOf(target)
|
|
if v.Kind() != reflect.Ptr || v.IsNil() {
|
|
return fmt.Errorf("target must be non-nil pointer to struct")
|
|
}
|
|
v = v.Elem()
|
|
if v.Kind() != reflect.Struct {
|
|
return fmt.Errorf("target must be pointer to struct")
|
|
}
|
|
|
|
// 4. Helper: set field if exists and compatible
|
|
setField := func(fieldName string, value any) {
|
|
f := v.FieldByName(fieldName)
|
|
if !f.IsValid() || !f.CanSet() {
|
|
return // field not found or unexported
|
|
}
|
|
ft := f.Type()
|
|
vv := reflect.ValueOf(value)
|
|
|
|
// Case: field is *T, value is T → wrap in pointer
|
|
if ft.Kind() == reflect.Ptr {
|
|
elemType := ft.Elem()
|
|
if vv.Type().AssignableTo(elemType) {
|
|
ptr := reflect.New(elemType)
|
|
ptr.Elem().Set(vv)
|
|
f.Set(ptr)
|
|
}
|
|
// nil → leave as zero (nil pointer)
|
|
} else if vv.Type().AssignableTo(ft) {
|
|
f.Set(vv)
|
|
}
|
|
// else: type mismatch → silently skip (safe)
|
|
}
|
|
|
|
// 5. Dispatch by sort type
|
|
switch sortBy {
|
|
case "id":
|
|
setField("CursorID", id)
|
|
|
|
case "year":
|
|
setField("CursorID", id)
|
|
param, err := extractString(payload, "param")
|
|
if err != nil {
|
|
return fmt.Errorf("cursor year: %w", err)
|
|
}
|
|
year, err := strconv.Atoi(param)
|
|
if err != nil {
|
|
return fmt.Errorf("cursor year: param must be integer, got %q", param)
|
|
}
|
|
setField("CursorYear", int32(year)) // or int, depending on your schema
|
|
|
|
case "rating":
|
|
setField("CursorID", id)
|
|
param, err := extractString(payload, "param")
|
|
if err != nil {
|
|
return fmt.Errorf("cursor rating: %w", err)
|
|
}
|
|
rating, err := strconv.ParseFloat(param, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("cursor rating: param must be float, got %q", param)
|
|
}
|
|
setField("CursorRating", rating)
|
|
|
|
default:
|
|
return fmt.Errorf("unsupported sort_by: %q", sortBy)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// --- helpers ---
|
|
func decodeCursor(cursorStr string) (map[string]any, error) {
|
|
data, err := base64.RawURLEncoding.DecodeString(cursorStr)
|
|
if err != nil {
|
|
data, err = base64.StdEncoding.DecodeString(cursorStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid base64 cursor")
|
|
}
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
return nil, fmt.Errorf("invalid cursor JSON: %w", err)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func extractInt64(m map[string]any, key string) (int64, error) {
|
|
v, ok := m[key]
|
|
if !ok {
|
|
return 0, fmt.Errorf("missing %q", key)
|
|
}
|
|
switch x := v.(type) {
|
|
case float64:
|
|
if x == float64(int64(x)) {
|
|
return int64(x), nil
|
|
}
|
|
case string:
|
|
i, err := strconv.ParseInt(x, 10, 64)
|
|
if err == nil {
|
|
return i, nil
|
|
}
|
|
case int64, int, int32:
|
|
return reflect.ValueOf(x).Int(), nil
|
|
}
|
|
return 0, fmt.Errorf("%q must be integer", key)
|
|
}
|
|
|
|
func extractString(m map[string]interface{}, key string) (string, error) {
|
|
v, ok := m[key]
|
|
if !ok {
|
|
return "", fmt.Errorf("missing %q", key)
|
|
}
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("%q must be string", key)
|
|
}
|
|
return s, nil
|
|
}
|