use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use serde::Serialize;
pub const DEFAULT_LIMIT: usize = 100;
pub const MAX_LIMIT: usize = 1000;
#[derive(Debug, Clone, Copy)]
pub struct Pagination {
pub limit: usize,
pub offset: usize,
}
#[derive(Debug, Clone)]
pub struct PaginationError {
pub field: &'static str,
pub message: String,
}
impl PaginationError {
pub fn new(field: &'static str, message: impl Into<String>) -> Self {
Self { field, message: message.into() }
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PaginationErrorResponse {
pub error: &'static str,
pub message: String,
pub details: PaginationErrorDetails,
}
#[derive(Debug, Clone, Serialize)]
pub struct PaginationErrorDetails {
pub field: &'static str,
}
pub fn parse_pagination(raw_query: Option<&str>) -> Result<Pagination, PaginationError> {
let mut limit = None;
let mut offset = None;
if let Some(query) = raw_query {
for segment in query.split('&') {
if segment.is_empty() {
continue;
}
let mut parts = segment.splitn(2, '=');
let key = parts.next().unwrap_or("");
let value = parts.next().unwrap_or("");
match key {
"limit" => {
limit = Some(parse_limit(value)?);
}
"offset" => {
offset = Some(parse_offset(value)?);
}
_ => {}
}
}
}
Ok(Pagination { limit: limit.unwrap_or(DEFAULT_LIMIT), offset: offset.unwrap_or(0) })
}
pub fn parse_limit(value: &str) -> Result<usize, PaginationError> {
let parsed = parse_non_negative(value, "limit")?;
if parsed == 0 || parsed > MAX_LIMIT {
return Err(PaginationError::new(
"limit",
format!("limit must be between 1 and {MAX_LIMIT}"),
));
}
Ok(parsed)
}
pub fn parse_offset(value: &str) -> Result<usize, PaginationError> {
parse_non_negative(value, "offset")
}
pub fn parse_non_negative(value: &str, field: &'static str) -> Result<usize, PaginationError> {
if value.is_empty() {
return Err(PaginationError::new(field, format!("{field} must be a non-negative integer")));
}
value
.parse::<usize>()
.map_err(|_| PaginationError::new(field, format!("{field} must be a non-negative integer")))
}
pub fn pagination_error(error: PaginationError) -> impl IntoResponse {
let payload = PaginationErrorResponse {
error: "invalid_pagination",
message: error.message,
details: PaginationErrorDetails { field: error.field },
};
(StatusCode::UNPROCESSABLE_ENTITY, Json(payload))
}