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}