use crate::exception::{Error, Result};
use async_trait::async_trait;
use super::core::{AsyncPaginator, PaginatedResponse, Paginator, SchemaParameter};
#[derive(Debug, Clone)]
pub struct LimitOffsetPagination {
pub default_limit: usize,
pub limit_query_param: String,
pub offset_query_param: String,
pub max_limit: Option<usize>,
}
impl Default for LimitOffsetPagination {
fn default() -> Self {
Self {
default_limit: 10,
limit_query_param: "limit".to_string(),
offset_query_param: "offset".to_string(),
max_limit: None,
}
}
}
impl LimitOffsetPagination {
pub fn new() -> Self {
Self::default()
}
pub fn default_limit(mut self, limit: usize) -> Self {
self.default_limit = limit;
self
}
pub fn max_limit(mut self, limit: usize) -> Self {
self.max_limit = Some(limit);
self
}
fn parse_positive_int(value: &str) -> Result<usize> {
value
.parse::<usize>()
.map_err(|_| Error::Validation(format!("Invalid number: {}", value)))
}
fn parse_params(&self, params: &str, _base_url: &str) -> Result<(usize, usize)> {
let query_string = if params.starts_with("http") || params.starts_with('/') {
if let Ok(url) = url::Url::parse(params)
.or_else(|_| url::Url::parse(&format!("http://localhost{}", params)))
{
url.query().unwrap_or("").to_string()
} else {
params.to_string()
}
} else {
params.to_string()
};
let mut limit = self.default_limit;
let mut offset = 0;
for pair in query_string.split('&') {
let parts: Vec<&str> = pair.split('=').collect();
if parts.len() == 2 {
let key = parts[0];
let value = parts[1];
if key == self.limit_query_param {
limit = Self::parse_positive_int(value)?;
if limit == 0 {
return Err(Error::InvalidLimit(format!(
"{} must be greater than zero (got {})",
self.limit_query_param, limit
)));
}
if let Some(max) = self.max_limit
&& limit > max
{
return Err(Error::InvalidLimit(format!(
"Limit {} exceeds maximum {}",
limit, max
)));
}
} else if key == self.offset_query_param {
offset = Self::parse_positive_int(value)?;
}
}
}
Ok((limit, offset))
}
fn build_url(&self, base_url: &str, offset: usize, limit: 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.offset_query_param, &offset.to_string())
.append_pair(&self.limit_query_param, &limit.to_string());
for (key, value) in url.query_pairs() {
if key != self.offset_query_param && key != self.limit_query_param {
new_url.query_pairs_mut().append_pair(&key, &value);
}
}
new_url.to_string()
}
}
#[async_trait]
impl Paginator for LimitOffsetPagination {
fn paginate<T: Clone + Send + Sync>(
&self,
items: &[T],
params: Option<&str>,
base_url: &str,
) -> Result<PaginatedResponse<T>> {
let (limit, offset) = if let Some(param_str) = params {
self.parse_params(param_str, base_url)?
} else {
(self.default_limit, 0)
};
let total_count = items.len();
if offset > total_count {
return Ok(PaginatedResponse {
count: total_count,
next: None,
previous: None,
results: vec![],
});
}
let start = offset;
let end = std::cmp::min(start + limit, total_count);
let results = items[start..end].to_vec();
let next = if end < total_count {
Some(self.build_url(base_url, offset + limit, limit))
} else {
None
};
let previous = if offset > 0 {
let prev_offset = offset.saturating_sub(limit);
Some(self.build_url(base_url, prev_offset, limit))
} else {
None
};
Ok(PaginatedResponse {
count: total_count,
next,
previous,
results,
})
}
fn get_schema_parameters(&self) -> Vec<SchemaParameter> {
vec![
SchemaParameter {
name: self.limit_query_param.clone(),
required: false,
location: "query".to_string(),
description: "Number of results to return per page.".to_string(),
schema_type: "integer".to_string(),
},
SchemaParameter {
name: self.offset_query_param.clone(),
required: false,
location: "query".to_string(),
description: "The initial index from which to return the results.".to_string(),
schema_type: "integer".to_string(),
},
]
}
}
#[async_trait]
impl AsyncPaginator for LimitOffsetPagination {
async fn apaginate<T: Clone + Send + Sync>(
&self,
items: &[T],
params: Option<&str>,
base_url: &str,
) -> Result<PaginatedResponse<T>> {
self.paginate(items, params, base_url)
}
fn get_schema_parameters(&self) -> Vec<SchemaParameter> {
Paginator::get_schema_parameters(self)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[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 = LimitOffsetPagination::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:?}"
);
}
}