use crate::exception::{Error, Result};
use async_trait::async_trait;
use super::core::{AsyncPaginator, Page, PaginatedResponse, Paginator, SchemaParameter};
#[derive(Debug, Clone)]
pub struct ErrorMessages {
pub invalid_page: String,
pub min_page: String,
pub no_results: String,
}
impl Default for ErrorMessages {
fn default() -> Self {
Self {
invalid_page: "Invalid page number".to_string(),
min_page: "That page number is less than 1".to_string(),
no_results: "That page contains no results".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct PageNumberPagination {
pub page_size: usize,
pub page_query_param: String,
pub page_size_query_param: Option<String>,
pub max_page_size: Option<usize>,
pub last_page_strings: Vec<String>,
pub orphans: usize,
pub allow_empty_first_page: bool,
pub error_messages: ErrorMessages,
}
impl Default for PageNumberPagination {
fn default() -> Self {
Self {
page_size: 10,
page_query_param: "page".to_string(),
page_size_query_param: None,
max_page_size: None,
last_page_strings: vec!["last".to_string()],
orphans: 0,
allow_empty_first_page: true,
error_messages: ErrorMessages::default(),
}
}
}
impl PageNumberPagination {
pub fn new() -> Self {
Self::default()
}
pub fn page_size(mut self, size: usize) -> Self {
self.page_size = if size == 0 { 1 } else { size };
self
}
pub fn max_page_size(mut self, size: usize) -> Self {
self.max_page_size = Some(size);
self
}
pub fn page_size_query_param(mut self, param: impl Into<String>) -> Self {
self.page_size_query_param = Some(param.into());
self
}
pub fn orphans(mut self, orphans: usize) -> Self {
self.orphans = orphans;
self
}
pub fn allow_empty_first_page(mut self, allow: bool) -> Self {
self.allow_empty_first_page = allow;
self
}
pub fn error_messages(mut self, messages: ErrorMessages) -> Self {
self.error_messages = messages;
self
}
pub fn get_page<T: Clone>(&self, items: &[T], page_param: Option<&str>) -> Page<T> {
let total_count = items.len();
let effective_page_size = self.page_size;
let total_pages = if total_count <= effective_page_size {
1
} else {
let pages = total_count / effective_page_size;
let remainder = total_count % effective_page_size;
if remainder > 0 && remainder <= self.orphans {
pages
} else if remainder > 0 {
pages + 1
} else {
pages
}
};
let page_number = page_param
.and_then(|p| self.parse_page_number(p, total_pages).ok())
.unwrap_or(1);
let page_number = if page_number > total_pages && total_count > 0 {
total_pages } else if page_number == 0 {
1
} else {
page_number
};
let (start, end) = if total_count == 0 {
(0, 0)
} else if page_number == total_pages {
let start = (page_number - 1) * effective_page_size;
(start, total_count)
} else {
let start = (page_number - 1) * effective_page_size;
let end = std::cmp::min(start + effective_page_size, total_count);
(start, end)
};
let object_list = items[start..end].to_vec();
Page {
object_list,
number: page_number,
num_pages: total_pages,
count: total_count,
page_size: effective_page_size,
}
}
pub async fn aget_page<T: Clone + Send + Sync>(
&self,
items: &[T],
page_param: Option<&str>,
) -> Page<T> {
self.get_page(items, page_param)
}
fn parse_page_number(&self, page_str: &str, total_pages: usize) -> Result<usize> {
if self.last_page_strings.iter().any(|s| s == page_str) {
return Ok(total_pages);
}
if let Ok(n) = page_str.parse::<usize>() {
if n == 0 {
return Err(Error::InvalidPage(self.error_messages.min_page.clone()));
}
return Ok(n);
}
if let Ok(f) = page_str.parse::<f64>() {
if f.fract() == 0.0 && f > 0.0 {
let n = f as usize;
if n == 0 {
return Err(Error::InvalidPage(self.error_messages.min_page.clone()));
}
return Ok(n);
}
}
Err(Error::InvalidPage(self.error_messages.invalid_page.clone()))
}
fn build_url(&self, base_url: &str, page: usize) -> String {
let url = super::parse_base_url(base_url);
let mut new_url = url.clone();
new_url
.query_pairs_mut()
.clear()
.append_pair(&self.page_query_param, &page.to_string());
for (key, value) in url.query_pairs() {
if key != self.page_query_param {
new_url.query_pairs_mut().append_pair(&key, &value);
}
}
new_url.to_string()
}
}
#[async_trait]
impl Paginator for PageNumberPagination {
fn paginate<T: Clone + Send + Sync>(
&self,
items: &[T],
page_param: Option<&str>,
base_url: &str,
) -> Result<PaginatedResponse<T>> {
let total_count = items.len();
if total_count == 0 && !self.allow_empty_first_page {
return Err(Error::InvalidPage(self.error_messages.no_results.clone()));
}
let total_pages = if total_count == 0 {
if self.allow_empty_first_page { 1 } else { 0 }
} else {
if total_count <= self.page_size {
1
} else {
let pages = total_count / self.page_size;
let remainder = total_count % self.page_size;
if remainder > 0 && remainder <= self.orphans {
pages
} else if remainder > 0 {
pages + 1
} else {
pages
}
}
};
let page_number = if let Some(param) = page_param {
self.parse_page_number(param, total_pages)?
} else {
1
};
if page_number > total_pages && total_count > 0 {
return Err(Error::InvalidPage(self.error_messages.no_results.clone()));
}
let (start, end) = if total_count == 0 {
(0, 0)
} else if page_number == total_pages {
let start = (page_number - 1) * self.page_size;
(start, total_count)
} else {
let start = (page_number - 1) * self.page_size;
let end = std::cmp::min(start + self.page_size, total_count);
(start, end)
};
let results = items[start..end].to_vec();
let next = if page_number < total_pages {
Some(self.build_url(base_url, page_number + 1))
} else {
None
};
let previous = if page_number > 1 {
Some(self.build_url(base_url, page_number - 1))
} else {
None
};
Ok(PaginatedResponse {
count: total_count,
next,
previous,
results,
})
}
fn get_schema_parameters(&self) -> Vec<SchemaParameter> {
let mut params = vec![SchemaParameter {
name: self.page_query_param.clone(),
required: false,
location: "query".to_string(),
description: "A page number within the paginated result set.".to_string(),
schema_type: "integer".to_string(),
}];
if let Some(ref page_size_param) = self.page_size_query_param {
params.push(SchemaParameter {
name: page_size_param.clone(),
required: false,
location: "query".to_string(),
description: "Number of results to return per page.".to_string(),
schema_type: "integer".to_string(),
});
}
params
}
}
#[async_trait]
impl AsyncPaginator for PageNumberPagination {
async fn apaginate<T: Clone + Send + Sync>(
&self,
items: &[T],
page_param: Option<&str>,
base_url: &str,
) -> Result<PaginatedResponse<T>> {
self.paginate(items, page_param, base_url)
}
fn get_schema_parameters(&self) -> Vec<SchemaParameter> {
Paginator::get_schema_parameters(self)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn page_size_setter_normalizes_zero_to_one() {
let paginator = PageNumberPagination::new().page_size(0);
assert_eq!(paginator.page_size, 1);
}
#[rstest]
fn paginate_works_when_page_size_set_to_zero() {
let paginator = PageNumberPagination::new().page_size(0);
let items: Vec<i32> = (1..=5).collect();
let result = paginator.paginate(&items, None, "http://example.com/items");
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.results, vec![1]);
assert_eq!(response.count, 5);
}
#[rstest]
fn get_page_works_when_page_size_set_to_zero() {
let paginator = PageNumberPagination::new().page_size(0);
let items: Vec<i32> = (1..=5).collect();
let page = paginator.get_page(&items, Some("1"));
assert_eq!(page.page_size, 1);
assert_eq!(page.count, 5);
}
#[rstest]
fn paginate_works_with_page_size_one() {
let paginator = PageNumberPagination::new().page_size(1);
let items: Vec<i32> = (1..=3).collect();
let result = paginator
.paginate(&items, Some("2"), "http://example.com/items")
.unwrap();
assert_eq!(result.results, vec![2]);
assert_eq!(result.count, 3);
}
#[rstest]
#[case("not a valid url at all \x00\x01")]
#[case("://missing-scheme")]
#[case("")]
fn build_url_does_not_panic_with_malformed_base_url(#[case] malformed_url: &str) {
let paginator = PageNumberPagination::new();
let items: Vec<i32> = (0..20).collect();
let result = paginator.paginate(&items, None, malformed_url);
assert!(
result.is_ok(),
"paginate should not panic with malformed URL: {malformed_url:?}"
);
}
}