use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PagedResponse<T> {
pub items: Vec<T>,
pub total_count: Option<u64>,
pub pagination: Pagination,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pagination {
pub next: Option<String>,
pub prev: Option<String>,
pub first: Option<String>,
pub last: Option<String>,
pub page: Option<u64>,
pub per_page: Option<u64>,
}
impl Pagination {
pub fn has_next(&self) -> bool {
self.next.is_some()
}
pub fn has_prev(&self) -> bool {
self.prev.is_some()
}
pub fn next_page(&self) -> Option<u64> {
self.page.map(|p| p + 1)
}
pub fn prev_page(&self) -> Option<u64> {
self.page
.and_then(|p| if p > 1 { Some(p - 1) } else { None })
}
}
impl Default for Pagination {
fn default() -> Self {
Self {
next: None,
prev: None,
first: None,
last: None,
page: Some(1),
per_page: Some(30), }
}
}
impl<T> PagedResponse<T> {
pub fn has_next(&self) -> bool {
self.pagination.has_next()
}
pub fn next_page_number(&self) -> Option<u32> {
self.pagination
.next
.as_ref()
.and_then(|url| extract_page_number(url))
}
pub fn is_last_page(&self) -> bool {
!self.has_next()
}
}
pub fn parse_link_header(link_header: Option<&str>) -> Pagination {
let mut pagination = Pagination::default();
if let Some(header) = link_header {
for link in header.split(',') {
let parts: Vec<&str> = link.split(';').collect();
if parts.len() != 2 {
continue;
}
let url = parts[0]
.trim()
.trim_start_matches('<')
.trim_end_matches('>');
let rel = parts[1]
.trim()
.trim_start_matches("rel=\"")
.trim_end_matches('"');
match rel {
"next" => pagination.next = Some(url.to_string()),
"prev" => pagination.prev = Some(url.to_string()),
"first" => pagination.first = Some(url.to_string()),
"last" => pagination.last = Some(url.to_string()),
_ => {}
}
}
}
pagination
}
pub fn extract_page_number(url: &str) -> Option<u32> {
url.split('?')
.nth(1) .and_then(|query| {
query.split('&').find_map(|param| {
let mut parts = param.split('=');
let key = parts.next()?;
let value = parts.next()?;
if key == "page" {
value.parse::<u32>().ok()
} else {
None
}
})
})
}
#[cfg(test)]
#[path = "pagination_tests.rs"]
mod tests;