#[cfg(test)]
mod tests;
#[derive(Debug, Clone, PartialEq)]
pub struct Page<T> {
pub items: Vec<T>,
pub page: u64,
pub per_page: u64,
pub total_items: u64,
pub total_pages: u64,
}
impl<T> Page<T> {
pub fn new(items: Vec<T>, page: u64, per_page: u64, total_items: u64) -> Self {
let page = page.max(1);
let per_page = per_page.max(1);
let total_pages = if total_items == 0 { 0 } else { (total_items + per_page - 1) / per_page };
Page { items, page, per_page, total_items, total_pages }
}
pub fn has_next(&self) -> bool {
self.page < self.total_pages
}
pub fn has_prev(&self) -> bool {
self.page > 1 && self.total_pages > 0
}
pub fn next_page(&self) -> Option<u64> {
self.has_next().then_some(self.page + 1)
}
pub fn prev_page(&self) -> Option<u64> {
self.has_prev().then_some(self.page - 1)
}
pub fn map<U>(self, mut f: impl FnMut(T) -> U) -> Page<U> {
Page {
items: self.items.into_iter().map(|item| f(item)).collect(),
page: self.page,
per_page: self.per_page,
total_items: self.total_items,
total_pages: self.total_pages,
}
}
pub fn link_header(&self, base_url: &str) -> Option<String> {
let mut links = Vec::new();
if self.has_prev() {
if let Some(url) = with_query_params(base_url, &[("page", "1"), ("per_page", &self.per_page.to_string())]) {
links.push(format!(r#"<{}>; rel="first""#, url));
}
if let Some(url) = with_query_params(base_url, &[("page", &(self.page - 1).to_string()), ("per_page", &self.per_page.to_string())]) {
links.push(format!(r#"<{}>; rel="prev""#, url));
}
}
if self.has_next() {
if let Some(url) = with_query_params(base_url, &[("page", &(self.page + 1).to_string()), ("per_page", &self.per_page.to_string())]) {
links.push(format!(r#"<{}>; rel="next""#, url));
}
if let Some(url) = with_query_params(base_url, &[("page", &self.total_pages.to_string()), ("per_page", &self.per_page.to_string())]) {
links.push(format!(r#"<{}>; rel="last""#, url));
}
}
if links.is_empty() { None } else { Some(links.join(", ")) }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CursorPage<T> {
pub items: Vec<T>,
pub next_cursor: Option<String>,
}
impl<T> CursorPage<T> {
pub fn has_next(&self) -> bool {
self.next_cursor.is_some()
}
pub fn map<U>(self, mut f: impl FnMut(T) -> U) -> CursorPage<U> {
CursorPage {
items: self.items.into_iter().map(|item| f(item)).collect(),
next_cursor: self.next_cursor,
}
}
pub fn link_header(&self, base_url: &str, cursor_param: &str) -> Option<String> {
let cursor = self.next_cursor.as_ref()?;
let url = with_query_params(base_url, &[(cursor_param, cursor.as_str())])?;
Some(format!(r#"<{}>; rel="next""#, url))
}
}
fn with_query_params(base_url: &str, params: &[(&str, &str)]) -> Option<String> {
let mut components = crate::url::URL::parse(base_url).ok()?;
let mut query = components.query.take().unwrap_or_default();
for (key, value) in params {
query.insert(key.to_string(), value.to_string());
}
components.query = Some(query);
crate::url::URL::build(components).ok()
}