this/core/query.rs
1//! Query parameters and pagination utilities
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// Query parameters for pagination and filtering
7///
8/// This structure is used to extract pagination and filtering parameters
9/// from URL query strings. All parameters have sensible defaults.
10///
11/// # Example
12/// ```rust,ignore
13/// // In handler:
14/// pub async fn list_items(
15/// Query(params): Query<QueryParams>,
16/// ) -> Json<PaginatedResponse<Item>> {
17/// // params.page defaults to 1
18/// // params.limit defaults to 20
19/// }
20///
21/// // Usage:
22/// GET /items?page=2&limit=10
23/// GET /items?filter={"status": "active"}
24/// GET /items?page=1&limit=20&filter={"amount>": 100}&sort=created_at:desc
25/// ```
26#[derive(Debug, Clone, Deserialize)]
27#[serde(default)]
28pub struct QueryParams {
29 /// Page number (starts at 1)
30 #[serde(default = "default_page")]
31 pub page: usize,
32
33 /// Number of items per page
34 #[serde(default = "default_limit")]
35 pub limit: usize,
36
37 /// Filters as JSON object
38 ///
39 /// # Format
40 /// - Exact match: `{"field": "value"}`
41 /// - Comparison: `{"field>": value, "field<": value, "field>=": value, "field<=": value}`
42 ///
43 /// # Example
44 /// ```text
45 /// filter={"status": "active", "amount>": 100, "customer_name": "Acme"}
46 /// ```
47 pub filter: Option<String>,
48
49 /// Sort field and direction
50 ///
51 /// # Format
52 /// - `field:asc` or `field` (ascending)
53 /// - `field:desc` (descending)
54 ///
55 /// # Example
56 /// ```text
57 /// sort=amount:desc
58 /// sort=created_at:asc
59 /// ```
60 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 /// Get page number, ensuring minimum of 1
84 pub fn page(&self) -> usize {
85 self.page.max(1)
86 }
87
88 /// Get limit, ensuring it doesn't exceed the maximum
89 pub fn limit(&self) -> usize {
90 self.limit.clamp(1, 100) // Maximum 100 per page, minimum 1
91 }
92
93 /// Parse filter JSON string into Value
94 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/// Paginated response structure
102///
103/// This structure wraps paginated data with metadata about pagination state.
104#[derive(Debug, Serialize)]
105pub struct PaginatedResponse<T> {
106 /// The paginated data
107 pub data: Vec<T>,
108
109 /// Pagination metadata
110 pub pagination: PaginationMeta,
111}
112
113/// Pagination metadata
114#[derive(Debug, Serialize)]
115pub struct PaginationMeta {
116 /// Current page number (starts at 1)
117 pub page: usize,
118
119 /// Number of items per page
120 pub limit: usize,
121
122 /// Total number of items (after filters)
123 pub total: usize,
124
125 /// Total number of pages
126 pub total_pages: usize,
127
128 /// Whether there is a next page
129 pub has_next: bool,
130
131 /// Whether there is a previous page
132 pub has_prev: bool,
133}
134
135impl PaginationMeta {
136 /// Create pagination metadata from calculation
137 pub fn new(page: usize, limit: usize, total: usize) -> Self {
138 // Ensure limit is at least 1 to avoid division by zero
139 let limit = limit.max(1);
140 let total_pages = if total == 0 { 0 } else { total.div_ceil(limit) }; // Ceiling division
141 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}