@@ -65,7 +65,7 @@ fn body_url_field(ctx: &PageContext<'_>) -> Option<String> {
6565fn cursor_based ( ctx : & PageContext < ' _ > ) -> Option < String > {
6666 let obj = ctx. body . as_object ( ) ?;
6767
68- // Check for has_more: true
68+ // Check for has_more / hasMore / has_next_page : true
6969 let has_more = obj. get ( "has_more" )
7070 . or_else ( || obj. get ( "hasMore" ) )
7171 . or_else ( || obj. get ( "has_next_page" ) ) ?
@@ -75,25 +75,67 @@ fn cursor_based(ctx: &PageContext<'_>) -> Option<String> {
7575 return None ;
7676 }
7777
78- // Get the last item's ID from the data array
79- let items = ctx. data . as_array ( ) ?;
80- let last_item = items. last ( ) ?;
81- let cursor = last_item. get ( "id" )
82- . and_then ( |v| v. as_str ( ) . map ( |s| s. to_owned ( ) ) )
83- . or_else ( || last_item. get ( "id" ) . and_then ( |v| v. as_i64 ( ) . map ( |n| n. to_string ( ) ) ) ) ?;
78+ // Determine cursor value. Priority:
79+ // 1. Explicit cursor field in the response body (next_cursor, cursor, end_cursor)
80+ // 2. Last item's id field (Stripe and most REST APIs)
81+ let cursor = extract_explicit_cursor ( obj)
82+ . or_else ( || extract_last_item_id ( ctx. data ) ) ?;
8483
85- // Determine the cursor parameter name
86- let cursor_param = if obj. contains_key ( "has_more" ) || obj. contains_key ( "hasMore" ) {
87- "starting_after" // Stripe convention
88- } else {
89- "after" // Generic cursor convention
90- } ;
84+ // Determine the cursor parameter name from the response shape
85+ let cursor_param = detect_cursor_param ( obj) ;
9186
9287 // Append cursor to URL
9388 let separator = if ctx. current_url . contains ( '?' ) { "&" } else { "?" } ;
9489 Some ( format ! ( "{}{separator}{cursor_param}={cursor}" , ctx. current_url) )
9590}
9691
92+ /// Look for an explicit cursor/token in the response body.
93+ fn extract_explicit_cursor ( obj : & serde_json:: Map < String , serde_json:: Value > ) -> Option < String > {
94+ for key in [ "next_cursor" , "cursor" , "end_cursor" , "ending_before" ,
95+ "next_page_token" , "pageToken" , "continuation_token" ] {
96+ if let Some ( val) = obj. get ( key) {
97+ match val {
98+ serde_json:: Value :: String ( s) if !s. is_empty ( ) => return Some ( s. clone ( ) ) ,
99+ serde_json:: Value :: Number ( n) => return Some ( n. to_string ( ) ) ,
100+ _ => { }
101+ }
102+ }
103+ }
104+ None
105+ }
106+
107+ /// Fall back to the last item's ID in the data array.
108+ fn extract_last_item_id ( data : & serde_json:: Value ) -> Option < String > {
109+ let items = data. as_array ( ) ?;
110+ let last = items. last ( ) ?;
111+ for key in [ "id" , "_id" , "uuid" ] {
112+ if let Some ( val) = last. get ( key) {
113+ match val {
114+ serde_json:: Value :: String ( s) => return Some ( s. clone ( ) ) ,
115+ serde_json:: Value :: Number ( n) => return Some ( n. to_string ( ) ) ,
116+ _ => { }
117+ }
118+ }
119+ }
120+ None
121+ }
122+
123+ /// Detect the right query parameter name for the cursor.
124+ fn detect_cursor_param ( obj : & serde_json:: Map < String , serde_json:: Value > ) -> & ' static str {
125+ // If the response has an explicit cursor field, match the param name
126+ if obj. contains_key ( "next_cursor" ) || obj. contains_key ( "cursor" ) {
127+ return "cursor" ;
128+ }
129+ if obj. contains_key ( "next_page_token" ) || obj. contains_key ( "pageToken" ) {
130+ return "page_token" ;
131+ }
132+ if obj. contains_key ( "continuation_token" ) {
133+ return "continuation_token" ;
134+ }
135+ // Default: Stripe convention
136+ "starting_after"
137+ }
138+
97139#[ cfg( test) ]
98140mod tests {
99141 use super :: * ;
0 commit comments