mirror of
https://github.com/perstarkse/minne.git
synced 2026-06-30 01:51:43 +02:00
208 lines
5.7 KiB
Rust
208 lines
5.7 KiB
Rust
use serde::Serialize;
|
|
|
|
/// Metadata describing a paginated collection.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct Pagination {
|
|
pub current_page: usize,
|
|
pub per_page: usize,
|
|
pub total_items: usize,
|
|
pub total_pages: usize,
|
|
pub has_previous: bool,
|
|
pub has_next: bool,
|
|
pub previous_page: Option<usize>,
|
|
pub next_page: Option<usize>,
|
|
pub start_index: usize,
|
|
pub end_index: usize,
|
|
}
|
|
|
|
impl Pagination {
|
|
pub const fn new(
|
|
current_page: usize,
|
|
per_page: usize,
|
|
total_items: usize,
|
|
total_pages: usize,
|
|
page_len: usize,
|
|
) -> Self {
|
|
let has_pages = total_pages > 0;
|
|
let has_previous = has_pages && current_page > 1;
|
|
let has_next = has_pages && current_page < total_pages;
|
|
let offset = if has_pages {
|
|
per_page.saturating_mul(current_page.saturating_sub(1))
|
|
} else {
|
|
0
|
|
};
|
|
let start_index = if page_len == 0 {
|
|
0
|
|
} else {
|
|
offset.saturating_add(1)
|
|
};
|
|
let end_index = if page_len == 0 {
|
|
0
|
|
} else {
|
|
offset.saturating_add(page_len)
|
|
};
|
|
|
|
Self {
|
|
current_page,
|
|
per_page,
|
|
total_items,
|
|
total_pages,
|
|
has_previous,
|
|
has_next,
|
|
previous_page: if has_previous {
|
|
Some(current_page.saturating_sub(1))
|
|
} else {
|
|
None
|
|
},
|
|
next_page: if has_next {
|
|
Some(current_page.saturating_add(1))
|
|
} else {
|
|
None
|
|
},
|
|
start_index,
|
|
end_index,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a cloned page slice and pagination metadata without consuming the source list.
|
|
pub fn paginate_slice<T: Clone>(
|
|
items: &[T],
|
|
requested_page: Option<usize>,
|
|
per_page: usize,
|
|
) -> (Vec<T>, Pagination) {
|
|
let per_page = per_page.max(1);
|
|
let total_items = items.len();
|
|
let total_pages = if total_items == 0 {
|
|
0
|
|
} else {
|
|
total_items
|
|
.saturating_sub(1)
|
|
.checked_div(per_page)
|
|
.unwrap_or(0)
|
|
.saturating_add(1)
|
|
};
|
|
|
|
let mut current_page = requested_page.unwrap_or(1);
|
|
if current_page == 0 {
|
|
current_page = 1;
|
|
}
|
|
if total_pages > 0 {
|
|
current_page = current_page.min(total_pages);
|
|
} else {
|
|
current_page = 1;
|
|
}
|
|
|
|
let offset = if total_pages == 0 {
|
|
0
|
|
} else {
|
|
per_page.saturating_mul(current_page.saturating_sub(1))
|
|
};
|
|
|
|
let page_items: Vec<T> = items.iter().skip(offset).take(per_page).cloned().collect();
|
|
let page_len = page_items.len();
|
|
let pagination = Pagination::new(current_page, per_page, total_items, total_pages, page_len);
|
|
|
|
(page_items, pagination)
|
|
}
|
|
|
|
/// Returns the items for the requested page along with pagination metadata.
|
|
pub fn paginate_items<T>(
|
|
items: Vec<T>,
|
|
requested_page: Option<usize>,
|
|
per_page: usize,
|
|
) -> (Vec<T>, Pagination) {
|
|
let per_page = per_page.max(1);
|
|
let total_items = items.len();
|
|
let total_pages = if total_items == 0 {
|
|
0
|
|
} else {
|
|
total_items
|
|
.saturating_sub(1)
|
|
.checked_div(per_page)
|
|
.unwrap_or(0)
|
|
.saturating_add(1)
|
|
};
|
|
|
|
let mut current_page = requested_page.unwrap_or(1);
|
|
if current_page == 0 {
|
|
current_page = 1;
|
|
}
|
|
if total_pages > 0 {
|
|
current_page = current_page.min(total_pages);
|
|
} else {
|
|
current_page = 1;
|
|
}
|
|
|
|
let offset = if total_pages == 0 {
|
|
0
|
|
} else {
|
|
per_page.saturating_mul(current_page.saturating_sub(1))
|
|
};
|
|
|
|
let page_items: Vec<T> = items.into_iter().skip(offset).take(per_page).collect();
|
|
let page_len = page_items.len();
|
|
let pagination = Pagination::new(current_page, per_page, total_items, total_pages, page_len);
|
|
|
|
(page_items, pagination)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{paginate_items, paginate_slice};
|
|
|
|
#[test]
|
|
fn paginates_basic_case() {
|
|
let items: Vec<_> = (1..=25).collect();
|
|
let (page, meta) = paginate_items(items, Some(2), 10);
|
|
|
|
assert_eq!(page, vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
|
|
assert_eq!(meta.current_page, 2);
|
|
assert_eq!(meta.total_pages, 3);
|
|
assert!(meta.has_previous);
|
|
assert!(meta.has_next);
|
|
assert_eq!(meta.previous_page, Some(1));
|
|
assert_eq!(meta.next_page, Some(3));
|
|
assert_eq!(meta.start_index, 11);
|
|
assert_eq!(meta.end_index, 20);
|
|
}
|
|
|
|
#[test]
|
|
fn handles_empty_items() {
|
|
let items: Vec<u8> = vec![];
|
|
let (page, meta) = paginate_items(items, Some(3), 10);
|
|
|
|
assert!(page.is_empty());
|
|
assert_eq!(meta.current_page, 1);
|
|
assert_eq!(meta.total_pages, 0);
|
|
assert!(!meta.has_previous);
|
|
assert!(!meta.has_next);
|
|
assert_eq!(meta.start_index, 0);
|
|
assert_eq!(meta.end_index, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn paginate_slice_clones_only_page_items() {
|
|
let items: Vec<_> = (1..=25).collect();
|
|
let (page, meta) = paginate_slice(&items, Some(2), 10);
|
|
|
|
assert_eq!(page, vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
|
|
assert_eq!(items.len(), 25);
|
|
assert_eq!(meta.current_page, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn clamps_page_to_bounds() {
|
|
let items: Vec<_> = (1..=5).collect();
|
|
let (page, meta) = paginate_items(items, Some(10), 2);
|
|
|
|
assert_eq!(page, vec![5]);
|
|
assert_eq!(meta.current_page, 3);
|
|
assert_eq!(meta.total_pages, 3);
|
|
assert!(!meta.has_next, "expected no next page");
|
|
assert!(meta.has_previous, "expected previous page");
|
|
assert_eq!(meta.start_index, 5);
|
|
assert_eq!(meta.end_index, 5);
|
|
}
|
|
}
|