use std::fmt;
use serde::{Deserialize, Serialize};
pub const DEFAULT_PER_PAGE: u32 = 20;
pub const MAX_PER_PAGE: u32 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SortOrder {
#[default]
Asc,
Desc,
}
impl fmt::Display for SortOrder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Asc => write!(f, "asc"),
Self::Desc => write!(f, "desc"),
}
}
}
impl SortOrder {
#[must_use]
pub const fn as_sql(&self) -> &'static str {
match self {
Self::Asc => "ASC",
Self::Desc => "DESC",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct ListQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub per_page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<SortOrder>,
#[serde(skip_serializing_if = "Option::is_none")]
pub search: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub filter: Vec<String>,
}
impl ListQuery {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_page(mut self, page: u32) -> Self {
self.page = Some(page);
self
}
#[must_use]
pub fn with_per_page(mut self, per_page: u32) -> Self {
self.per_page = Some(per_page);
self
}
#[must_use]
pub fn with_sort(mut self, sort: String) -> Self {
self.sort = Some(sort);
self
}
#[must_use]
pub fn with_order(mut self, order: SortOrder) -> Self {
self.order = Some(order);
self
}
#[must_use]
pub fn with_search(mut self, search: String) -> Self {
self.search = Some(search);
self
}
#[must_use]
pub fn with_filter(mut self, filter: String) -> Self {
self.filter.push(filter);
self
}
#[must_use]
pub fn with_filters(mut self, filters: Vec<String>) -> Self {
self.filter = filters;
self
}
#[must_use]
pub fn page_number(&self) -> u32 {
self.page.unwrap_or(1).max(1)
}
#[must_use]
pub fn items_per_page(&self) -> u32 {
self.per_page
.unwrap_or(DEFAULT_PER_PAGE)
.clamp(1, MAX_PER_PAGE)
}
#[must_use]
pub fn offset(&self) -> u64 {
u64::from(self.page_number().saturating_sub(1)) * u64::from(self.items_per_page())
}
#[must_use]
pub fn sort_order(&self) -> SortOrder {
self.order.unwrap_or_default()
}
#[must_use]
pub fn has_search(&self) -> bool {
self.search.as_ref().is_some_and(|s| !s.is_empty())
}
#[must_use]
pub fn has_filters(&self) -> bool {
!self.filter.is_empty()
}
#[must_use]
pub fn has_sort(&self) -> bool {
self.sort.as_ref().is_some_and(|s| !s.is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sort_order_display() {
assert_eq!(format!("{}", SortOrder::Asc), "asc");
assert_eq!(format!("{}", SortOrder::Desc), "desc");
}
#[test]
fn test_sort_order_default() {
assert_eq!(SortOrder::default(), SortOrder::Asc);
}
#[test]
fn test_sort_order_as_sql() {
assert_eq!(SortOrder::Asc.as_sql(), "ASC");
assert_eq!(SortOrder::Desc.as_sql(), "DESC");
}
#[test]
fn test_list_query_default() {
let query = ListQuery::default();
assert!(query.page.is_none());
assert!(query.per_page.is_none());
assert!(query.sort.is_none());
assert!(query.order.is_none());
assert!(query.search.is_none());
assert!(query.filter.is_empty());
}
#[test]
fn test_list_query_new() {
let query = ListQuery::new();
assert_eq!(query, ListQuery::default());
}
#[test]
fn test_list_query_with_page() {
let query = ListQuery::new().with_page(5);
assert_eq!(query.page, Some(5));
assert_eq!(query.page_number(), 5);
}
#[test]
fn test_list_query_with_per_page() {
let query = ListQuery::new().with_per_page(50);
assert_eq!(query.per_page, Some(50));
assert_eq!(query.items_per_page(), 50);
}
#[test]
fn test_list_query_with_sort() {
let query = ListQuery::new().with_sort("name".to_string());
assert_eq!(query.sort, Some("name".to_string()));
assert!(query.has_sort());
}
#[test]
fn test_list_query_with_order() {
let query = ListQuery::new().with_order(SortOrder::Desc);
assert_eq!(query.order, Some(SortOrder::Desc));
assert_eq!(query.sort_order(), SortOrder::Desc);
}
#[test]
fn test_list_query_with_search() {
let query = ListQuery::new().with_search("alice".to_string());
assert_eq!(query.search, Some("alice".to_string()));
assert!(query.has_search());
}
#[test]
fn test_list_query_with_filter() {
let query = ListQuery::new()
.with_filter("status=active".to_string())
.with_filter("role=admin".to_string());
assert_eq!(query.filter.len(), 2);
assert!(query.has_filters());
}
#[test]
fn test_list_query_with_filters() {
let query = ListQuery::new()
.with_filters(vec!["status=active".to_string(), "role=admin".to_string()]);
assert_eq!(query.filter.len(), 2);
}
#[test]
fn test_page_number_defaults() {
let query = ListQuery::new();
assert_eq!(query.page_number(), 1);
}
#[test]
fn test_page_number_zero_protection() {
let query = ListQuery::new().with_page(0);
assert_eq!(query.page_number(), 1);
}
#[test]
fn test_items_per_page_defaults() {
let query = ListQuery::new();
assert_eq!(query.items_per_page(), DEFAULT_PER_PAGE);
}
#[test]
fn test_items_per_page_zero_protection() {
let query = ListQuery::new().with_per_page(0);
assert_eq!(query.items_per_page(), 1);
}
#[test]
fn test_items_per_page_max_limit() {
let query = ListQuery::new().with_per_page(500);
assert_eq!(query.items_per_page(), MAX_PER_PAGE);
}
#[test]
fn test_offset_calculation() {
let query = ListQuery::new().with_page(1).with_per_page(20);
assert_eq!(query.offset(), 0);
let query = ListQuery::new().with_page(2).with_per_page(20);
assert_eq!(query.offset(), 20);
let query = ListQuery::new().with_page(3).with_per_page(50);
assert_eq!(query.offset(), 100);
}
#[test]
fn test_sort_order_defaults() {
let query = ListQuery::new();
assert_eq!(query.sort_order(), SortOrder::Asc);
}
#[test]
fn test_has_search_empty() {
let query = ListQuery::new();
assert!(!query.has_search());
}
#[test]
fn test_has_search_empty_string() {
let query = ListQuery::new().with_search(String::new());
assert!(!query.has_search());
}
#[test]
fn test_has_search_with_value() {
let query = ListQuery::new().with_search("test".to_string());
assert!(query.has_search());
}
#[test]
fn test_has_filters_empty() {
let query = ListQuery::new();
assert!(!query.has_filters());
}
#[test]
fn test_has_filters_with_value() {
let query = ListQuery::new().with_filter("status=active".to_string());
assert!(query.has_filters());
}
#[test]
fn test_has_sort_empty() {
let query = ListQuery::new();
assert!(!query.has_sort());
}
#[test]
fn test_has_sort_empty_string() {
let query = ListQuery::new().with_sort(String::new());
assert!(!query.has_sort());
}
#[test]
fn test_has_sort_with_value() {
let query = ListQuery::new().with_sort("name".to_string());
assert!(query.has_sort());
}
#[test]
fn test_list_query_clone() {
let query = ListQuery::new()
.with_page(2)
.with_per_page(50)
.with_sort("name".to_string())
.with_order(SortOrder::Desc)
.with_search("test".to_string())
.with_filter("status=active".to_string());
let cloned = query.clone();
assert_eq!(query, cloned);
}
#[test]
fn test_list_query_chained_builder() {
let query = ListQuery::new()
.with_page(2)
.with_per_page(50)
.with_sort("created_at".to_string())
.with_order(SortOrder::Desc)
.with_search("alice".to_string())
.with_filter("status=active".to_string())
.with_filter("role=admin".to_string());
assert_eq!(query.page_number(), 2);
assert_eq!(query.items_per_page(), 50);
assert_eq!(query.offset(), 50);
assert_eq!(query.sort, Some("created_at".to_string()));
assert_eq!(query.sort_order(), SortOrder::Desc);
assert_eq!(query.search, Some("alice".to_string()));
assert_eq!(query.filter.len(), 2);
}
#[test]
fn test_sort_order_serde() {
let asc: SortOrder = serde_json::from_str("\"asc\"").unwrap();
assert_eq!(asc, SortOrder::Asc);
let desc: SortOrder = serde_json::from_str("\"desc\"").unwrap();
assert_eq!(desc, SortOrder::Desc);
assert_eq!(serde_json::to_string(&SortOrder::Asc).unwrap(), "\"asc\"");
assert_eq!(serde_json::to_string(&SortOrder::Desc).unwrap(), "\"desc\"");
}
#[test]
fn test_list_query_serde() {
let query = ListQuery::new()
.with_page(2)
.with_per_page(50)
.with_sort("name".to_string())
.with_order(SortOrder::Desc);
let json = serde_json::to_string(&query).unwrap();
let deserialized: ListQuery = serde_json::from_str(&json).unwrap();
assert_eq!(query, deserialized);
}
}