feat: cursor implemented
This commit is contained in:
parent
9c0fada00e
commit
af0492cdf1
8 changed files with 435 additions and 106 deletions
156
modules/backend/handlers/cursor.go
Normal file
156
modules/backend/handlers/cursor.go
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue