use crate::Error;
use std::{collections::HashSet, future::Future};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PaginationLinks {
pub first: String,
pub last: String,
pub prev: Option<String>,
pub next: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PaginationMetaLink {
pub url: Option<String>,
pub label: String,
pub active: bool,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PaginationMeta {
pub current_page: i64,
pub from: Option<i64>,
pub last_page: i64,
pub links: Vec<PaginationMetaLink>,
pub path: String,
pub per_page: i64,
pub to: Option<i64>,
pub total: i64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Page<T> {
pub data: Vec<T>,
pub links: PaginationLinks,
pub meta: PaginationMeta,
}
impl<T> Page<T> {
pub fn next_page_number(
&self,
base_url: &str,
operation_path: &str,
) -> Result<Option<u64>, Error> {
if self.meta.current_page >= self.meta.last_page {
return Ok(None);
}
let Some(link) = self.links.next.as_deref() else {
return Ok(Some((self.meta.current_page + 1) as u64));
};
let base =
reqwest::Url::parse(base_url).map_err(|e| Error::Configuration(e.to_string()))?;
let next = base
.join(link)
.map_err(|e| Error::Serialization(format!("malformed next-page link: {e}")))?;
if next.scheme() != base.scheme()
|| next.host_str() != base.host_str()
|| next.port_or_known_default() != base.port_or_known_default()
|| next.fragment().is_some()
|| !next.username().is_empty()
|| next.password().is_some()
{
return Err(Error::Serialization("unsafe next-page origin".into()));
}
let expected = base
.join(operation_path.trim_start_matches('/'))
.map_err(|e| Error::Serialization(e.to_string()))?;
if next.path() != expected.path() {
return Err(Error::Serialization(
"next-page operation path changed".into(),
));
}
let raw = next
.query_pairs()
.find(|(key, _)| key == "page")
.map(|(_, value)| value.into_owned())
.ok_or_else(|| Error::Serialization("next-page link has no page number".into()))?;
let page = raw
.parse::<u64>()
.map_err(|_| Error::Serialization("malformed next-page number".into()))?;
if page == 0 {
return Err(Error::Serialization("page number must be positive".into()));
}
Ok(Some(page))
}
}
pub fn auto_paginate_pages<T, E, F, Fut>(
mut fetch: F,
) -> impl futures_util::Stream<Item = Result<T, E>>
where
E: From<Error>,
F: FnMut(Option<String>) -> Fut,
Fut: Future<Output = Result<(Vec<T>, Option<String>), E>>,
{
async_stream::try_stream! {let mut cursor=None;let mut seen=HashSet::new();loop{let(items,next)=fetch(cursor).await?;for item in items{yield item}match next{Some(value)=>{if value.trim().is_empty()||!seen.insert(value.clone()){Err(E::from(Error::Serialization("invalid or repeated pagination cursor".into())))?}cursor=Some(value)},None=>break}}}
}
pub fn auto_paginate_numbered<T, F, Fut>(
mut fetch: F,
max_pages: usize,
max_items: usize,
) -> impl futures_util::Stream<Item = Result<T, Error>>
where
F: FnMut(u64) -> Fut,
Fut: Future<Output = Result<(Vec<T>, Option<u64>), Error>>,
{
async_stream::try_stream! {let(mut page,mut pages,mut items)=(1u64,0usize,0usize);let mut seen=HashSet::new();loop{if pages>=max_pages||items>=max_items{break};if !seen.insert(page){Err(Error::Serialization("pagination cycle detected".into()))?}let(values,next)=fetch(page).await?;pages+=1;for value in values{if items>=max_items{break}items+=1;yield value}match next{Some(value)=>page=value,None=>break}}}
}