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 for SQL query
33    /// Caps at 100 for typical API calls, but allows up to 10000 for internal batch operations
34    pub fn limit(&self) -> i64 {
35        self.per_page.min(10000)
36    }
37
38    /// Validate page request parameters
39    /// For external API calls, per_page should be <= 100 (enforced by handlers)
40    /// For internal use cases (e.g., financial reports), allows up to 10000
41    pub fn validate(&self) -> Result<(), String> {
42        if self.page < 1 {
43            return Err("page must be >= 1".to_string());
44        }
45        if self.per_page < 1 || self.per_page > 10000 {
46            return Err("per_page must be between 1 and 10000".to_string());
47        }
48        Ok(())
49    }
50
51    /// Validate page request parameters for external API calls
52    /// Enforces stricter limit of 100 items per page
53    pub fn validate_api(&self) -> Result<(), String> {
54        if self.page < 1 {
55            return Err("page must be >= 1".to_string());
56        }
57        if self.per_page < 1 || self.per_page > 100 {
58            return Err("per_page must be between 1 and 100".to_string());
59        }
60        Ok(())
61    }
62}
63
64impl Default for PageRequest {
65    fn default() -> Self {
66        Self {
67            page: 1,
68            per_page: 20,
69            sort_by: None,
70            order: SortOrder::default(),
71        }
72    }
73}
74
75/// Sort order for pagination
76#[derive(Debug, Deserialize, Serialize, Default, Clone)]
77#[serde(rename_all = "lowercase")]
78pub enum SortOrder {
79    #[default]
80    Asc,
81    Desc,
82}
83
84impl SortOrder {
85    /// Convert to SQL ORDER BY clause
86    pub fn to_sql(&self) -> &str {
87        match self {
88            SortOrder::Asc => "ASC",
89            SortOrder::Desc => "DESC",
90        }
91    }
92}
93
94/// Paginated response wrapper
95#[derive(Debug, Serialize)]
96pub struct PageResponse<T> {
97    pub data: Vec<T>,
98    pub pagination: PaginationMeta,
99}
100
101impl<T> PageResponse<T> {
102    pub fn new(data: Vec<T>, page: i64, per_page: i64, total_items: i64) -> Self {
103        Self {
104            data,
105            pagination: PaginationMeta::new(page, per_page, total_items),
106        }
107    }
108}
109
110/// Pagination metadata
111#[derive(Debug, Serialize)]
112pub struct PaginationMeta {
113    pub current_page: i64,
114    pub per_page: i64,
115    pub total_items: i64,
116    pub total_pages: i64,
117    pub has_next: bool,
118    pub has_previous: bool,
119}
120
121impl PaginationMeta {
122    pub fn new(current_page: i64, per_page: i64, total_items: i64) -> Self {
123        let total_pages = if total_items == 0 {
124            0
125        } else {
126            ((total_items as f64) / (per_page as f64)).ceil() as i64
127        };
128
129        Self {
130            current_page,
131            per_page,
132            total_items,
133            total_pages,
134            has_next: current_page < total_pages,
135            has_previous: current_page > 1,
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_page_request_default() {
146        let req = PageRequest::default();
147        assert_eq!(req.page, 1);
148        assert_eq!(req.per_page, 20);
149        assert_eq!(req.offset(), 0);
150        assert_eq!(req.limit(), 20);
151    }
152
153    #[test]
154    fn test_page_request_offset() {
155        let req = PageRequest {
156            page: 3,
157            per_page: 20,
158            sort_by: None,
159            order: SortOrder::default(),
160        };
161        assert_eq!(req.offset(), 40); // (3-1) * 20 = 40
162    }
163
164    #[test]
165    fn test_page_request_limit_capped() {
166        let req = PageRequest {
167            page: 1,
168            per_page: 20000, // Excessive
169            sort_by: None,
170            order: SortOrder::default(),
171        };
172        assert_eq!(req.limit(), 10000); // Capped at 10000
173    }
174
175    #[test]
176    fn test_page_request_validation_valid() {
177        let req = PageRequest {
178            page: 1,
179            per_page: 20,
180            sort_by: None,
181            order: SortOrder::default(),
182        };
183        assert!(req.validate().is_ok());
184    }
185
186    #[test]
187    fn test_page_request_validation_invalid_page() {
188        let req = PageRequest {
189            page: 0, // Invalid
190            per_page: 20,
191            sort_by: None,
192            order: SortOrder::default(),
193        };
194        assert!(req.validate().is_err());
195    }
196
197    #[test]
198    fn test_page_request_validation_invalid_per_page() {
199        let req = PageRequest {
200            page: 1,
201            per_page: 10001, // Invalid (> 10000)
202            sort_by: None,
203            order: SortOrder::default(),
204        };
205        assert!(req.validate().is_err());
206    }
207
208    #[test]
209    fn test_page_request_validation_api_invalid_per_page() {
210        let req = PageRequest {
211            page: 1,
212            per_page: 101, // Invalid for API (> 100)
213            sort_by: None,
214            order: SortOrder::default(),
215        };
216        assert!(req.validate_api().is_err());
217    }
218
219    #[test]
220    fn test_page_request_validation_internal_large_per_page() {
221        let req = PageRequest {
222            page: 1,
223            per_page: 10000, // Valid for internal use (financial reports)
224            sort_by: None,
225            order: SortOrder::default(),
226        };
227        assert!(req.validate().is_ok());
228        // limit() should allow up to 10000 for internal use
229        assert_eq!(req.limit(), 10000);
230    }
231
232    #[test]
233    fn test_pagination_meta_calculation() {
234        let meta = PaginationMeta::new(2, 20, 45);
235        assert_eq!(meta.current_page, 2);
236        assert_eq!(meta.per_page, 20);
237        assert_eq!(meta.total_items, 45);
238        assert_eq!(meta.total_pages, 3); // ceil(45/20) = 3
239        assert!(meta.has_next);
240        assert!(meta.has_previous);
241    }
242
243    #[test]
244    fn test_pagination_meta_first_page() {
245        let meta = PaginationMeta::new(1, 20, 100);
246        assert!(!meta.has_previous);
247        assert!(meta.has_next);
248    }
249
250    #[test]
251    fn test_pagination_meta_last_page() {
252        let meta = PaginationMeta::new(5, 20, 100);
253        assert!(meta.has_previous);
254        assert!(!meta.has_next);
255    }
256
257    #[test]
258    fn test_pagination_meta_empty() {
259        let meta = PaginationMeta::new(1, 20, 0);
260        assert_eq!(meta.total_pages, 0);
261        assert!(!meta.has_next);
262        assert!(!meta.has_previous);
263    }
264
265    #[test]
266    fn test_sort_order_to_sql() {
267        assert_eq!(SortOrder::Asc.to_sql(), "ASC");
268        assert_eq!(SortOrder::Desc.to_sql(), "DESC");
269    }
270}