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