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.Pointer || 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.Pointer { 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]any, 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 }