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 {
34 self.per_page.min(100)
35 }
36
37 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#[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 pub fn to_sql(&self) -> &str {
72 match self {
73 SortOrder::Asc => "ASC",
74 SortOrder::Desc => "DESC",
75 }
76 }
77}
78
79#[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#[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); }
148
149 #[test]
150 fn test_page_request_limit_capped() {
151 let req = PageRequest {
152 page: 1,
153 per_page: 500, sort_by: None,
155 order: SortOrder::default(),
156 };
157 assert_eq!(req.limit(), 100); }
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, 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, 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); 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}