1pub mod jobs;
52pub mod queues;
53pub mod stats;
54pub mod system;
55
56use serde::{Deserialize, Serialize};
57
58#[derive(Debug, Serialize)]
60pub struct ApiResponse<T> {
61 pub success: bool,
62 pub data: Option<T>,
63 pub error: Option<String>,
64 pub timestamp: chrono::DateTime<chrono::Utc>,
65}
66
67impl<T> ApiResponse<T> {
68 pub fn success(data: T) -> Self {
69 Self {
70 success: true,
71 data: Some(data),
72 error: None,
73 timestamp: chrono::Utc::now(),
74 }
75 }
76
77 pub fn error(message: String) -> Self {
78 Self {
79 success: false,
80 data: None,
81 error: Some(message),
82 timestamp: chrono::Utc::now(),
83 }
84 }
85}
86
87#[derive(Debug, Deserialize)]
89pub struct PaginationParams {
90 pub page: Option<u32>,
91 pub limit: Option<u32>,
92 pub offset: Option<u32>,
93}
94
95impl Default for PaginationParams {
96 fn default() -> Self {
97 Self {
98 page: Some(1),
99 limit: Some(50),
100 offset: None,
101 }
102 }
103}
104
105impl PaginationParams {
106 pub fn get_offset(&self) -> u32 {
107 if let Some(offset) = self.offset {
108 offset
109 } else {
110 let page = self.page.unwrap_or(1);
111 let limit = self.limit.unwrap_or(50);
112 (page.saturating_sub(1)) * limit
113 }
114 }
115
116 pub fn get_limit(&self) -> u32 {
117 self.limit.unwrap_or(50).min(1000) }
119}
120
121#[derive(Debug, Serialize)]
123pub struct PaginationMeta {
124 pub page: u32,
125 pub limit: u32,
126 pub offset: u32,
127 pub total: u64,
128 pub total_pages: u32,
129 pub has_next: bool,
130 pub has_prev: bool,
131}
132
133impl PaginationMeta {
134 pub fn new(params: &PaginationParams, total: u64) -> Self {
135 let limit = params.get_limit();
136 let offset = params.get_offset();
137 let page = params.page.unwrap_or(1);
138 let total_pages = ((total as f64) / (limit as f64)).ceil() as u32;
139
140 Self {
141 page,
142 limit,
143 offset,
144 total,
145 total_pages,
146 has_next: page < total_pages,
147 has_prev: page > 1,
148 }
149 }
150}
151
152#[derive(Debug, Serialize)]
154pub struct PaginatedResponse<T> {
155 pub items: Vec<T>,
156 pub pagination: PaginationMeta,
157}
158
159#[derive(Debug, Deserialize, Default)]
161pub struct FilterParams {
162 pub status: Option<String>,
163 pub priority: Option<String>,
164 pub queue: Option<String>,
165 pub created_after: Option<chrono::DateTime<chrono::Utc>>,
166 pub created_before: Option<chrono::DateTime<chrono::Utc>>,
167 pub search: Option<String>,
168}
169
170#[derive(Debug, Deserialize, Default)]
172pub struct SortParams {
173 pub sort_by: Option<String>,
174 pub sort_order: Option<String>,
175}
176
177impl SortParams {
178 pub fn get_order_by(&self) -> (String, String) {
179 let field = self.sort_by.as_deref().unwrap_or("created_at").to_string();
180 let direction = match self.sort_order.as_deref() {
181 Some("asc") | Some("ASC") => "ASC".to_string(),
182 _ => "DESC".to_string(),
183 };
184 (field, direction)
185 }
186}
187
188pub async fn handle_api_error(
190 err: warp::Rejection,
191) -> Result<impl warp::Reply, std::convert::Infallible> {
192 let response = if err.is_not_found() {
193 ApiResponse::<()>::error("Resource not found".to_string())
194 } else if err
195 .find::<warp::filters::body::BodyDeserializeError>()
196 .is_some()
197 {
198 ApiResponse::<()>::error("Invalid request body".to_string())
199 } else if err.find::<warp::reject::InvalidQuery>().is_some() {
200 ApiResponse::<()>::error("Invalid query parameters".to_string())
201 } else {
202 ApiResponse::<()>::error("Internal server error".to_string())
203 };
204
205 let status = if err.is_not_found() {
206 warp::http::StatusCode::NOT_FOUND
207 } else if err
208 .find::<warp::filters::body::BodyDeserializeError>()
209 .is_some()
210 || err.find::<warp::reject::InvalidQuery>().is_some()
211 {
212 warp::http::StatusCode::BAD_REQUEST
213 } else {
214 warp::http::StatusCode::INTERNAL_SERVER_ERROR
215 };
216
217 Ok(warp::reply::with_status(
218 warp::reply::json(&response),
219 status,
220 ))
221}
222
223pub fn with_pagination()
225-> impl warp::Filter<Extract = (PaginationParams,), Error = warp::Rejection> + Clone {
226 warp::query::<PaginationParams>()
227}
228
229pub fn with_filters()
231-> impl warp::Filter<Extract = (FilterParams,), Error = warp::Rejection> + Clone {
232 warp::query::<FilterParams>()
233}
234
235pub fn with_sort() -> impl warp::Filter<Extract = (SortParams,), Error = warp::Rejection> + Clone {
237 warp::query::<SortParams>()
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_api_response_success() {
246 let response = ApiResponse::success("test data");
247 assert!(response.success);
248 assert_eq!(response.data, Some("test data"));
249 assert!(response.error.is_none());
250 }
251
252 #[test]
253 fn test_api_response_error() {
254 let response: ApiResponse<()> = ApiResponse::error("Something went wrong".to_string());
255 assert!(!response.success);
256 assert!(response.data.is_none());
257 assert_eq!(response.error, Some("Something went wrong".to_string()));
258 }
259
260 #[test]
261 fn test_pagination_params_defaults() {
262 let params = PaginationParams::default();
263 assert_eq!(params.get_limit(), 50);
264 assert_eq!(params.get_offset(), 0);
265 }
266
267 #[test]
268 fn test_pagination_params_calculation() {
269 let params = PaginationParams {
270 page: Some(3),
271 limit: Some(20),
272 offset: None,
273 };
274 assert_eq!(params.get_limit(), 20);
275 assert_eq!(params.get_offset(), 40); }
277
278 #[test]
279 fn test_pagination_meta() {
280 let params = PaginationParams {
281 page: Some(2),
282 limit: Some(10),
283 offset: None,
284 };
285 let meta = PaginationMeta::new(¶ms, 45);
286
287 assert_eq!(meta.page, 2);
288 assert_eq!(meta.limit, 10);
289 assert_eq!(meta.total, 45);
290 assert_eq!(meta.total_pages, 5);
291 assert!(meta.has_next);
292 assert!(meta.has_prev);
293 }
294
295 #[test]
296 fn test_sort_params_defaults() {
297 let params = SortParams {
298 sort_by: None,
299 sort_order: None,
300 };
301 let (field, direction) = params.get_order_by();
302 assert_eq!(field, "created_at");
303 assert_eq!(direction, "DESC");
304 }
305
306 #[test]
307 fn test_sort_params_custom() {
308 let params = SortParams {
309 sort_by: Some("name".to_string()),
310 sort_order: Some("asc".to_string()),
311 };
312 let (field, direction) = params.get_order_by();
313 assert_eq!(field, "name");
314 assert_eq!(direction, "ASC");
315 }
316}