use std::future::Future;
use futures::Stream;
use futures::TryStreamExt;
use futures::stream;
use serde::Deserialize;
use crate::error::Error;
#[derive(Debug, Clone)]
pub struct Page<T> {
pub items: Vec<T>,
pub has_next_page: bool,
pub end_cursor: Option<String>,
pub has_previous_page: bool,
pub start_cursor: Option<String>,
}
impl<T> Default for Page<T> {
fn default() -> Self {
Self {
items: Vec::new(),
has_next_page: false,
end_cursor: None,
has_previous_page: false,
start_cursor: None,
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase", default)]
pub struct PageInfo {
pub has_next_page: bool,
pub end_cursor: Option<String>,
pub has_previous_page: bool,
pub start_cursor: Option<String>,
}
pub fn paginate<T, F, Fut>(fetch_page: F) -> impl Stream<Item = Result<T, Error>>
where
T: 'static,
F: Fn(Option<String>) -> Fut + Clone + 'static,
Fut: Future<Output = Result<Page<T>, Error>>,
{
stream::unfold(
(None, true, fetch_page), |(cursor, has_next, fetch)| async move {
if !has_next {
return None;
}
match fetch(cursor).await {
Ok(page) => {
let items = stream::iter(page.items.into_iter().map(Ok));
Some((Ok(items), (page.end_cursor, page.has_next_page, fetch)))
}
Err(e) => {
Some((Err(e), (None, false, fetch)))
}
}
},
)
.try_flatten()
}
pub fn paginate_backward<T, F, Fut>(fetch_page: F) -> impl Stream<Item = Result<T, Error>>
where
T: 'static,
F: Fn(Option<String>) -> Fut + Clone + 'static,
Fut: Future<Output = Result<Page<T>, Error>>,
{
stream::unfold(
(None, true, fetch_page),
|(cursor, has_prev, fetch)| async move {
if !has_prev {
return None;
}
match fetch(cursor).await {
Ok(page) => {
let items = stream::iter(page.items.into_iter().map(Ok));
Some((
Ok(items),
(page.start_cursor, page.has_previous_page, fetch),
))
}
Err(e) => Some((Err(e), (None, false, fetch))),
}
},
)
.try_flatten()
}
#[cfg(test)]
mod tests {
use super::*;
use futures::StreamExt;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
#[test]
fn test_page_info_deserialization() {
let json = r#"{"hasNextPage": true, "endCursor": "abc123"}"#;
let page_info: PageInfo = serde_json::from_str(json).unwrap();
assert!(page_info.has_next_page);
assert_eq!(page_info.end_cursor, Some("abc123".to_string()));
}
#[test]
fn test_page_info_deserialization_no_cursor() {
let json = r#"{"hasNextPage": false, "endCursor": null}"#;
let page_info: PageInfo = serde_json::from_str(json).unwrap();
assert!(!page_info.has_next_page);
assert_eq!(page_info.end_cursor, None);
}
#[tokio::test]
async fn test_paginate_single_page() {
let stream = paginate(|_cursor| async {
Ok(Page {
items: vec![1, 2, 3],
..Default::default()
})
});
let results: Vec<_> = stream.collect().await;
assert_eq!(results.len(), 3);
assert_eq!(results[0].as_ref().unwrap(), &1);
assert_eq!(results[1].as_ref().unwrap(), &2);
assert_eq!(results[2].as_ref().unwrap(), &3);
}
#[tokio::test]
async fn test_paginate_multiple_pages() {
let page_count = Arc::new(AtomicUsize::new(0));
let stream = paginate({
let page_count = page_count.clone();
move |cursor| {
let page_count = page_count.clone();
async move {
let page_num = page_count.fetch_add(1, Ordering::SeqCst);
match page_num {
0 => {
assert!(cursor.is_none());
Ok(Page {
items: vec![1, 2],
has_next_page: true,
end_cursor: Some("cursor1".to_string()),
..Default::default()
})
}
1 => {
assert_eq!(cursor, Some("cursor1".to_string()));
Ok(Page {
items: vec![3, 4],
has_next_page: true,
end_cursor: Some("cursor2".to_string()),
..Default::default()
})
}
2 => {
assert_eq!(cursor, Some("cursor2".to_string()));
Ok(Page {
items: vec![5],
..Default::default()
})
}
_ => panic!("unexpected page request"),
}
}
}
});
let results: Vec<i32> = stream.map(|r| r.unwrap()).collect().await;
assert_eq!(results, vec![1, 2, 3, 4, 5]);
assert_eq!(page_count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn test_paginate_empty_page() {
let stream = paginate(|_cursor| async { Ok(Page::<i32>::default()) });
let results: Vec<_> = stream.collect().await;
assert!(results.is_empty());
}
#[tokio::test]
async fn test_paginate_backward_single_page() {
let stream = paginate_backward(|_cursor| async {
Ok(Page {
items: vec![1, 2, 3],
..Default::default()
})
});
let results: Vec<_> = stream.collect().await;
assert_eq!(results.len(), 3);
assert_eq!(results[0].as_ref().unwrap(), &1);
assert_eq!(results[1].as_ref().unwrap(), &2);
assert_eq!(results[2].as_ref().unwrap(), &3);
}
#[tokio::test]
async fn test_paginate_backward_multiple_pages() {
let page_count = Arc::new(AtomicUsize::new(0));
let stream = paginate_backward({
let page_count = page_count.clone();
move |cursor| {
let page_count = page_count.clone();
async move {
let page_num = page_count.fetch_add(1, Ordering::SeqCst);
match page_num {
0 => {
assert!(cursor.is_none());
Ok(Page {
items: vec![5, 4],
has_previous_page: true,
start_cursor: Some("cursor1".to_string()),
..Default::default()
})
}
1 => {
assert_eq!(cursor, Some("cursor1".to_string()));
Ok(Page {
items: vec![3, 2],
has_previous_page: true,
start_cursor: Some("cursor2".to_string()),
..Default::default()
})
}
2 => {
assert_eq!(cursor, Some("cursor2".to_string()));
Ok(Page {
items: vec![1],
..Default::default()
})
}
_ => panic!("unexpected page request"),
}
}
}
});
let results: Vec<i32> = stream.map(|r| r.unwrap()).collect().await;
assert_eq!(results, vec![5, 4, 3, 2, 1]);
assert_eq!(page_count.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn test_paginate_backward_empty_page() {
let stream = paginate_backward(|_cursor| async { Ok(Page::<i32>::default()) });
let results: Vec<_> = stream.collect().await;
assert!(results.is_empty());
}
}