koprogo_api/application/dto/
pagination.rs1use serde::{Deserialize, Serialize};
2
3#[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 pub fn offset(&self) -> i64 {
29 (self.page - 1) * self.per_page
30 }
31
32 pub fn limit(&self) -> i64 {
35 self.per_page.min(10000)
36 }
37
38 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 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#[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 pub fn to_sql(&self) -> &str {
87 match self {
88 SortOrder::Asc => "ASC",
89 SortOrder::Desc => "DESC",
90 }
91 }
92}
93
94#[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#[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); }
163
164 #[test]
165 fn test_page_request_limit_capped() {
166 let req = PageRequest {
167 page: 1,
168 per_page: 20000, sort_by: None,
170 order: SortOrder::default(),
171 };
172 assert_eq!(req.limit(), 10000); }
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, 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, 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, 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, sort_by: None,
225 order: SortOrder::default(),
226 };
227 assert!(req.validate().is_ok());
228 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); 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}