1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6#[derive(Debug, Clone, Deserialize)]
27#[serde(default)]
28pub struct QueryParams {
29 #[serde(default = "default_page")]
31 pub page: usize,
32
33 #[serde(default = "default_limit")]
35 pub limit: usize,
36
37 pub filter: Option<String>,
48
49 pub sort: Option<String>,
61}
62
63fn default_page() -> usize {
64 1
65}
66
67fn default_limit() -> usize {
68 20
69}
70
71impl Default for QueryParams {
72 fn default() -> Self {
73 Self {
74 page: default_page(),
75 limit: default_limit(),
76 filter: None,
77 sort: None,
78 }
79 }
80}
81
82impl QueryParams {
83 pub fn page(&self) -> usize {
85 self.page.max(1)
86 }
87
88 pub fn limit(&self) -> usize {
90 self.limit.clamp(1, 100) }
92
93 pub fn filter_value(&self) -> Option<Value> {
95 self.filter
96 .as_ref()
97 .and_then(|s| serde_json::from_str(s).ok())
98 }
99}
100
101#[derive(Debug, Serialize)]
105pub struct PaginatedResponse<T> {
106 pub data: Vec<T>,
108
109 pub pagination: PaginationMeta,
111}
112
113#[derive(Debug, Serialize)]
115pub struct PaginationMeta {
116 pub page: usize,
118
119 pub limit: usize,
121
122 pub total: usize,
124
125 pub total_pages: usize,
127
128 pub has_next: bool,
130
131 pub has_prev: bool,
133}
134
135impl PaginationMeta {
136 pub fn new(page: usize, limit: usize, total: usize) -> Self {
138 let limit = limit.max(1);
140 let total_pages = if total == 0 { 0 } else { total.div_ceil(limit) }; let start = (page - 1) * limit;
142
143 Self {
144 page,
145 limit,
146 total,
147 total_pages,
148 has_next: start + limit < total,
149 has_prev: page > 1,
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_query_params_defaults() {
160 let params = QueryParams::default();
161 assert_eq!(params.page(), 1);
162 assert_eq!(params.limit(), 20);
163 }
164
165 #[test]
166 fn test_pagination_meta() {
167 let meta = PaginationMeta::new(1, 20, 145);
168 assert_eq!(meta.total, 145);
169 assert_eq!(meta.total_pages, 8);
170 assert!(!meta.has_prev);
171 assert!(meta.has_next);
172 }
173
174 #[test]
177 fn test_query_params_page_zero_clamps_to_one() {
178 let params = QueryParams {
179 page: 0,
180 ..Default::default()
181 };
182 assert_eq!(params.page(), 1);
183 }
184
185 #[test]
186 fn test_query_params_page_positive_unchanged() {
187 let params = QueryParams {
188 page: 5,
189 ..Default::default()
190 };
191 assert_eq!(params.page(), 5);
192 }
193
194 #[test]
197 fn test_query_params_limit_zero_clamps_to_one() {
198 let params = QueryParams {
199 limit: 0,
200 ..Default::default()
201 };
202 assert_eq!(params.limit(), 1);
203 }
204
205 #[test]
206 fn test_query_params_limit_over_100_clamps_to_100() {
207 let params = QueryParams {
208 limit: 101,
209 ..Default::default()
210 };
211 assert_eq!(params.limit(), 100);
212 }
213
214 #[test]
215 fn test_query_params_limit_within_range() {
216 let params = QueryParams {
217 limit: 50,
218 ..Default::default()
219 };
220 assert_eq!(params.limit(), 50);
221 }
222
223 #[test]
226 fn test_filter_value_valid_json_object() {
227 let params = QueryParams {
228 filter: Some(r#"{"status": "active"}"#.to_string()),
229 ..Default::default()
230 };
231 let value = params
232 .filter_value()
233 .expect("valid JSON should parse successfully");
234 assert_eq!(value["status"], "active");
235 }
236
237 #[test]
238 fn test_filter_value_invalid_json_returns_none() {
239 let params = QueryParams {
240 filter: Some("not-json".to_string()),
241 ..Default::default()
242 };
243 assert!(params.filter_value().is_none());
244 }
245
246 #[test]
247 fn test_filter_value_none_returns_none() {
248 let params = QueryParams {
249 filter: None,
250 ..Default::default()
251 };
252 assert!(params.filter_value().is_none());
253 }
254
255 #[test]
258 fn test_pagination_meta_total_zero() {
259 let meta = PaginationMeta::new(1, 20, 0);
260 assert_eq!(meta.total_pages, 0);
261 assert!(!meta.has_next);
262 assert!(!meta.has_prev);
263 }
264
265 #[test]
266 fn test_pagination_meta_last_page() {
267 let meta = PaginationMeta::new(5, 20, 100);
269 assert_eq!(meta.total_pages, 5);
270 assert!(!meta.has_next);
271 assert!(meta.has_prev);
272 }
273
274 #[test]
275 fn test_pagination_meta_single_page() {
276 let meta = PaginationMeta::new(1, 20, 10);
277 assert_eq!(meta.total_pages, 1);
278 assert!(!meta.has_next);
279 assert!(!meta.has_prev);
280 }
281
282 #[test]
283 fn test_pagination_meta_middle_page() {
284 let meta = PaginationMeta::new(3, 10, 50);
285 assert_eq!(meta.total_pages, 5);
286 assert!(meta.has_next);
287 assert!(meta.has_prev);
288 }
289
290 #[test]
291 fn test_pagination_meta_limit_zero_treated_as_one() {
292 let meta = PaginationMeta::new(1, 0, 10);
294 assert_eq!(meta.limit, 1);
295 assert_eq!(meta.total_pages, 10);
296 }
297}