use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Deserialize)]
#[serde(default)]
pub struct QueryParams {
#[serde(default = "default_page")]
pub page: usize,
#[serde(default = "default_limit")]
pub limit: usize,
pub filter: Option<String>,
pub sort: Option<String>,
}
fn default_page() -> usize {
1
}
fn default_limit() -> usize {
20
}
impl Default for QueryParams {
fn default() -> Self {
Self {
page: default_page(),
limit: default_limit(),
filter: None,
sort: None,
}
}
}
impl QueryParams {
pub fn page(&self) -> usize {
self.page.max(1)
}
pub fn limit(&self) -> usize {
self.limit.clamp(1, 100) }
pub fn filter_value(&self) -> Option<Value> {
self.filter
.as_ref()
.and_then(|s| serde_json::from_str(s).ok())
}
}
#[derive(Debug, Serialize)]
pub struct PaginatedResponse<T> {
pub data: Vec<T>,
pub pagination: PaginationMeta,
}
#[derive(Debug, Serialize)]
pub struct PaginationMeta {
pub page: usize,
pub limit: usize,
pub total: usize,
pub total_pages: usize,
pub has_next: bool,
pub has_prev: bool,
}
impl PaginationMeta {
pub fn new(page: usize, limit: usize, total: usize) -> Self {
let limit = limit.max(1);
let total_pages = if total == 0 { 0 } else { total.div_ceil(limit) }; let start = (page - 1) * limit;
Self {
page,
limit,
total,
total_pages,
has_next: start + limit < total,
has_prev: page > 1,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_params_defaults() {
let params = QueryParams::default();
assert_eq!(params.page(), 1);
assert_eq!(params.limit(), 20);
}
#[test]
fn test_pagination_meta() {
let meta = PaginationMeta::new(1, 20, 145);
assert_eq!(meta.total, 145);
assert_eq!(meta.total_pages, 8);
assert!(!meta.has_prev);
assert!(meta.has_next);
}
#[test]
fn test_query_params_page_zero_clamps_to_one() {
let params = QueryParams {
page: 0,
..Default::default()
};
assert_eq!(params.page(), 1);
}
#[test]
fn test_query_params_page_positive_unchanged() {
let params = QueryParams {
page: 5,
..Default::default()
};
assert_eq!(params.page(), 5);
}
#[test]
fn test_query_params_limit_zero_clamps_to_one() {
let params = QueryParams {
limit: 0,
..Default::default()
};
assert_eq!(params.limit(), 1);
}
#[test]
fn test_query_params_limit_over_100_clamps_to_100() {
let params = QueryParams {
limit: 101,
..Default::default()
};
assert_eq!(params.limit(), 100);
}
#[test]
fn test_query_params_limit_within_range() {
let params = QueryParams {
limit: 50,
..Default::default()
};
assert_eq!(params.limit(), 50);
}
#[test]
fn test_filter_value_valid_json_object() {
let params = QueryParams {
filter: Some(r#"{"status": "active"}"#.to_string()),
..Default::default()
};
let value = params
.filter_value()
.expect("valid JSON should parse successfully");
assert_eq!(value["status"], "active");
}
#[test]
fn test_filter_value_invalid_json_returns_none() {
let params = QueryParams {
filter: Some("not-json".to_string()),
..Default::default()
};
assert!(params.filter_value().is_none());
}
#[test]
fn test_filter_value_none_returns_none() {
let params = QueryParams {
filter: None,
..Default::default()
};
assert!(params.filter_value().is_none());
}
#[test]
fn test_pagination_meta_total_zero() {
let meta = PaginationMeta::new(1, 20, 0);
assert_eq!(meta.total_pages, 0);
assert!(!meta.has_next);
assert!(!meta.has_prev);
}
#[test]
fn test_pagination_meta_last_page() {
let meta = PaginationMeta::new(5, 20, 100);
assert_eq!(meta.total_pages, 5);
assert!(!meta.has_next);
assert!(meta.has_prev);
}
#[test]
fn test_pagination_meta_single_page() {
let meta = PaginationMeta::new(1, 20, 10);
assert_eq!(meta.total_pages, 1);
assert!(!meta.has_next);
assert!(!meta.has_prev);
}
#[test]
fn test_pagination_meta_middle_page() {
let meta = PaginationMeta::new(3, 10, 50);
assert_eq!(meta.total_pages, 5);
assert!(meta.has_next);
assert!(meta.has_prev);
}
#[test]
fn test_pagination_meta_limit_zero_treated_as_one() {
let meta = PaginationMeta::new(1, 0, 10);
assert_eq!(meta.limit, 1);
assert_eq!(meta.total_pages, 10);
}
}