koprogo_api/application/dto/
pagination.rs

1use serde::{Deserialize, Serialize};
2
3/// Page request parameters for pagination
4#[derive(Debug, Deserialize, Clone)]
5pub struct PageRequest {
6    #[serde(default = "default_page")]
7    pub page: i64,
8
9    #[serde(default = "default_per_page")]
10    pub per_page: i64,
11
12    pub sort_by: Option<String>,
13
14    #[serde(default)]
15    pub order: SortOrder,
16}
17
18fn default_page() -> i64 {
19    1
20}
21
22fn default_per_page() -> i64 {
23    20
24}
25
26impl PageRequest {
27    /// Calculate offset for SQL query
28    pub fn offset(&self) -> i64 {
29        (self.page - 1) * self.per_page
30    }
31
32    /// Get limit with max cap of 100 items
33    pub fn limit(&self) -> i64 {
34        self.per_page.min(100)
35    }
36
37    /// Validate page request parameters
38    pub fn validate(&self) -> Result<(), String> {
39        if self.page < 1 {
40            return Err("page must be >= 1".to_string());
41        }
42        if self.per_page < 1 || self.per_page > 100 {
43            return Err("per_page must be between 1 and 100".to_string());
44        }
45        Ok(())
46    }
47}
48
49impl Default for PageRequest {
50    fn default() -> Self {
51        Self {
52            page: 1,
53            per_page: 20,
54            sort_by: None,
55            order: SortOrder::default(),
56        }
57    }
58}
59
60/// Sort order for pagination
61#[derive(Debug, Deserialize, Serialize, Default, Clone)]
62#[serde(rename_all = "lowercase")]
63pub enum SortOrder {
64    #[default]
65    Asc,
66    Desc,
67}
68
69impl SortOrder {
70    /// Convert to SQL ORDER BY clause
71    pub fn to_sql(&self) -> &str {
72        match self {
73            SortOrder::Asc => "ASC",
74            SortOrder::Desc => "DESC",
75        }
76    }
77}
78
79/// Paginated response wrapper
80#[derive(Debug, Serialize)]
81pub struct PageResponse<T> {
82    pub data: Vec<T>,
83    pub pagination: PaginationMeta,
84}
85
86impl<T> PageResponse<T> {
87    pub fn new(data: Vec<T>, page: i64, per_page: i64, total_items: i64) -> Self {
88        Self {
89            data,
90            pagination: PaginationMeta::new(page, per_page, total_items),
91        }
92    }
93}
94
95/// Pagination metadata
96#[derive(Debug, Serialize)]
97pub struct PaginationMeta {
98    pub current_page: i64,
99    pub per_page: i64,
100    pub total_items: i64,
101    pub total_pages: i64,
102    pub has_next: bool,
103    pub has_previous: bool,
104}
105
106impl PaginationMeta {
107    pub fn new(current_page: i64, per_page: i64, total_items: i64) -> Self {
108        let total_pages = if total_items == 0 {
109            0
110        } else {
111            ((total_items as f64) / (per_page as f64)).ceil() as i64
112        };
113
114        Self {
115            current_page,
116            per_page,
117            total_items,
118            total_pages,
119            has_next: current_page < total_pages,
120            has_previous: current_page > 1,
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_page_request_default() {
131        let req = PageRequest::default();
132        assert_eq!(req.page, 1);
133        assert_eq!(req.per_page, 20);
134        assert_eq!(req.offset(), 0);
135        assert_eq!(req.limit(), 20);
136    }
137
138    #[test]
139    fn test_page_request_offset() {
140        let req = PageRequest {
141            page: 3,
142            per_page: 20,
143            sort_by: None,
144            order: SortOrder::default(),
145        };
146        assert_eq!(req.offset(), 40); // (3-1) * 20 = 40
147    }
148
149    #[test]
150    fn test_page_request_limit_capped() {
151        let req = PageRequest {
152            page: 1,
153            per_page: 500, // Excessive
154            sort_by: None,
155            order: SortOrder::default(),
156        };
157        assert_eq!(req.limit(), 100); // Capped at 100
158    }
159
160    #[test]
161    fn test_page_request_validation_valid() {
162        let req = PageRequest {
163            page: 1,
164            per_page: 20,
165            sort_by: None,
166            order: SortOrder::default(),
167        };
168        assert!(req.validate().is_ok());
169    }
170
171    #[test]
172    fn test_page_request_validation_invalid_page() {
173        let req = PageRequest {
174            page: 0, // Invalid
175            per_page: 20,
176            sort_by: None,
177            order: SortOrder::default(),
178        };
179        assert!(req.validate().is_err());
180    }
181
182    #[test]
183    fn test_page_request_validation_invalid_per_page() {
184        let req = PageRequest {
185            page: 1,
186            per_page: 101, // Invalid (> 100)
187            sort_by: None,
188            order: SortOrder::default(),
189        };
190        assert!(req.validate().is_err());
191    }
192
193    #[test]
194    fn test_pagination_meta_calculation() {
195        let meta = PaginationMeta::new(2, 20, 45);
196        assert_eq!(meta.current_page, 2);
197        assert_eq!(meta.per_page, 20);
198        assert_eq!(meta.total_items, 45);
199        assert_eq!(meta.total_pages, 3); // ceil(45/20) = 3
200        assert!(meta.has_next);
201        assert!(meta.has_previous);
202    }
203
204    #[test]
205    fn test_pagination_meta_first_page() {
206        let meta = PaginationMeta::new(1, 20, 100);
207        assert!(!meta.has_previous);
208        assert!(meta.has_next);
209    }
210
211    #[test]
212    fn test_pagination_meta_last_page() {
213        let meta = PaginationMeta::new(5, 20, 100);
214        assert!(meta.has_previous);
215        assert!(!meta.has_next);
216    }
217
218    #[test]
219    fn test_pagination_meta_empty() {
220        let meta = PaginationMeta::new(1, 20, 0);
221        assert_eq!(meta.total_pages, 0);
222        assert!(!meta.has_next);
223        assert!(!meta.has_previous);
224    }
225
226    #[test]
227    fn test_sort_order_to_sql() {
228        assert_eq!(SortOrder::Asc.to_sql(), "ASC");
229        assert_eq!(SortOrder::Desc.to_sql(), "DESC");
230    }
231}