1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, Default)]
6pub struct PaginationRequest {
7 #[serde(default = "default_limit")]
9 pub limit: usize,
10
11 #[serde(default)]
13 pub offset: usize,
14}
15
16fn default_limit() -> usize {
17 20
18}
19
20impl PaginationRequest {
21 pub fn new(limit: usize, offset: usize) -> Self {
22 Self {
23 limit: limit.min(100),
24 offset,
25 }
26 }
27
28 pub fn clamped_limit(&self) -> usize {
30 self.limit.min(100)
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PaginationResponse {
37 pub total: usize,
39
40 pub count: usize,
42
43 pub offset: usize,
45
46 pub limit: usize,
48
49 pub has_more: bool,
51}
52
53impl PaginationResponse {
54 pub fn new(total: usize, count: usize, offset: usize, limit: usize) -> Self {
55 Self {
56 total,
57 count,
58 offset,
59 limit,
60 has_more: offset + count < total,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
67#[serde(rename_all = "snake_case")]
68pub enum SortDirection {
69 #[default]
70 Asc,
71 Desc,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct SortRequest {
77 pub field: String,
79
80 #[serde(default)]
82 pub direction: SortDirection,
83}
84
85impl SortRequest {
86 pub fn new(field: impl Into<String>, direction: SortDirection) -> Self {
87 Self {
88 field: field.into(),
89 direction,
90 }
91 }
92
93 pub fn asc(field: impl Into<String>) -> Self {
94 Self::new(field, SortDirection::Asc)
95 }
96
97 pub fn desc(field: impl Into<String>) -> Self {
98 Self::new(field, SortDirection::Desc)
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, Default)]
104pub struct TimeRangeFilter {
105 pub since: Option<DateTime<Utc>>,
107
108 pub until: Option<DateTime<Utc>>,
110
111 pub as_of: Option<DateTime<Utc>>,
113}
114
115impl TimeRangeFilter {
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn since(mut self, since: DateTime<Utc>) -> Self {
121 self.since = Some(since);
122 self
123 }
124
125 pub fn until(mut self, until: DateTime<Utc>) -> Self {
126 self.until = Some(until);
127 self
128 }
129
130 pub fn as_of(mut self, as_of: DateTime<Utc>) -> Self {
131 self.as_of = Some(as_of);
132 self
133 }
134
135 pub fn has_constraints(&self) -> bool {
137 self.since.is_some() || self.until.is_some() || self.as_of.is_some()
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum ErrorSeverity {
145 Warning,
146 Error,
147 Critical,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
153pub enum ErrorCode {
154 ValidationError,
156 InvalidInput,
157 MissingField,
158 InvalidFormat,
159
160 Unauthorized,
162 InvalidToken,
163 TokenExpired,
164
165 Forbidden,
167 InsufficientPermissions,
168 QuotaExceeded,
169
170 NotFound,
172 EntityNotFound,
173 ResourceNotFound,
174
175 Conflict,
177 DuplicateEntry,
178 ConcurrencyConflict,
179 VersionMismatch,
180
181 RateLimited,
183 TooManyRequests,
184
185 InternalError,
187 DatabaseError,
188 ServiceUnavailable,
189}
190
191impl ErrorCode {
192 pub fn http_status(&self) -> u16 {
194 match self {
195 ErrorCode::ValidationError
196 | ErrorCode::InvalidInput
197 | ErrorCode::MissingField
198 | ErrorCode::InvalidFormat => 400,
199
200 ErrorCode::Unauthorized | ErrorCode::InvalidToken | ErrorCode::TokenExpired => 401,
201
202 ErrorCode::Forbidden
203 | ErrorCode::InsufficientPermissions
204 | ErrorCode::QuotaExceeded => 403,
205
206 ErrorCode::NotFound | ErrorCode::EntityNotFound | ErrorCode::ResourceNotFound => 404,
207
208 ErrorCode::Conflict
209 | ErrorCode::DuplicateEntry
210 | ErrorCode::ConcurrencyConflict
211 | ErrorCode::VersionMismatch => 409,
212
213 ErrorCode::RateLimited | ErrorCode::TooManyRequests => 429,
214
215 ErrorCode::InternalError | ErrorCode::DatabaseError | ErrorCode::ServiceUnavailable => {
216 500
217 }
218 }
219 }
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct FieldError {
225 pub field: String,
227
228 pub message: String,
230
231 pub code: Option<String>,
233}
234
235impl FieldError {
236 pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
237 Self {
238 field: field.into(),
239 message: message.into(),
240 code: None,
241 }
242 }
243
244 pub fn with_code(mut self, code: impl Into<String>) -> Self {
245 self.code = Some(code.into());
246 self
247 }
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ErrorResponse {
253 pub code: ErrorCode,
255
256 pub message: String,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub details: Option<String>,
262
263 #[serde(skip_serializing_if = "Vec::is_empty", default)]
265 pub field_errors: Vec<FieldError>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub request_id: Option<String>,
270
271 pub timestamp: DateTime<Utc>,
273}
274
275impl ErrorResponse {
276 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
277 Self {
278 code,
279 message: message.into(),
280 details: None,
281 field_errors: Vec::new(),
282 request_id: None,
283 timestamp: Utc::now(),
284 }
285 }
286
287 pub fn with_details(mut self, details: impl Into<String>) -> Self {
288 self.details = Some(details.into());
289 self
290 }
291
292 pub fn with_field_errors(mut self, errors: Vec<FieldError>) -> Self {
293 self.field_errors = errors;
294 self
295 }
296
297 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
298 self.request_id = Some(request_id.into());
299 self
300 }
301
302 pub fn validation_error(message: impl Into<String>) -> Self {
304 Self::new(ErrorCode::ValidationError, message)
305 }
306
307 pub fn not_found(entity: impl Into<String>) -> Self {
309 Self::new(
310 ErrorCode::NotFound,
311 format!("{} not found", entity.into()),
312 )
313 }
314
315 pub fn conflict(message: impl Into<String>) -> Self {
317 Self::new(ErrorCode::Conflict, message)
318 }
319
320 pub fn internal_error() -> Self {
322 Self::new(ErrorCode::InternalError, "An internal error occurred")
323 }
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct SuccessResponse<T> {
329 pub data: T,
331
332 #[serde(skip_serializing_if = "Option::is_none")]
334 pub meta: Option<ResponseMeta>,
335}
336
337impl<T> SuccessResponse<T> {
338 pub fn new(data: T) -> Self {
339 Self { data, meta: None }
340 }
341
342 pub fn with_meta(mut self, meta: ResponseMeta) -> Self {
343 self.meta = Some(meta);
344 self
345 }
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct ResponseMeta {
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub processing_time_ms: Option<u64>,
354
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub api_version: Option<String>,
358
359 #[serde(skip_serializing_if = "Option::is_none")]
361 pub deprecation_warning: Option<String>,
362}
363
364impl ResponseMeta {
365 pub fn new() -> Self {
366 Self {
367 processing_time_ms: None,
368 api_version: None,
369 deprecation_warning: None,
370 }
371 }
372
373 pub fn with_processing_time(mut self, ms: u64) -> Self {
374 self.processing_time_ms = Some(ms);
375 self
376 }
377}
378
379impl Default for ResponseMeta {
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct PaginatedResponse<T> {
388 pub items: Vec<T>,
390
391 pub pagination: PaginationResponse,
393}
394
395impl<T> PaginatedResponse<T> {
396 pub fn new(items: Vec<T>, total: usize, offset: usize, limit: usize) -> Self {
397 let count = items.len();
398 Self {
399 items,
400 pagination: PaginationResponse::new(total, count, offset, limit),
401 }
402 }
403
404 pub fn empty() -> Self {
405 Self {
406 items: Vec::new(),
407 pagination: PaginationResponse::new(0, 0, 0, 20),
408 }
409 }
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct BatchItemResult<T> {
415 pub index: usize,
417
418 pub success: bool,
420
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub data: Option<T>,
424
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub error: Option<ErrorResponse>,
428}
429
430impl<T> BatchItemResult<T> {
431 pub fn success(index: usize, data: T) -> Self {
432 Self {
433 index,
434 success: true,
435 data: Some(data),
436 error: None,
437 }
438 }
439
440 pub fn failure(index: usize, error: ErrorResponse) -> Self {
441 Self {
442 index,
443 success: false,
444 data: None,
445 error: Some(error),
446 }
447 }
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct BatchResponse<T> {
453 pub results: Vec<BatchItemResult<T>>,
455
456 pub total: usize,
458
459 pub successful: usize,
461
462 pub failed: usize,
464}
465
466impl<T> BatchResponse<T> {
467 pub fn new(results: Vec<BatchItemResult<T>>) -> Self {
468 let total = results.len();
469 let successful = results.iter().filter(|r| r.success).count();
470 let failed = total - successful;
471
472 Self {
473 results,
474 total,
475 successful,
476 failed,
477 }
478 }
479
480 pub fn all_successful(&self) -> bool {
482 self.failed == 0
483 }
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct HealthResponse {
489 pub status: HealthStatus,
491
492 pub version: String,
494
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub uptime_seconds: Option<u64>,
498
499 #[serde(skip_serializing_if = "Vec::is_empty", default)]
501 pub components: Vec<ComponentHealth>,
502}
503
504#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(rename_all = "snake_case")]
507pub enum HealthStatus {
508 Healthy,
509 Degraded,
510 Unhealthy,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct ComponentHealth {
516 pub name: String,
518
519 pub status: HealthStatus,
521
522 #[serde(skip_serializing_if = "Option::is_none")]
524 pub message: Option<String>,
525
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub response_time_ms: Option<u64>,
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn test_pagination_request_clamping() {
537 let pagination = PaginationRequest::new(200, 0);
538 assert_eq!(pagination.clamped_limit(), 100);
539 }
540
541 #[test]
542 fn test_pagination_response_has_more() {
543 let response = PaginationResponse::new(100, 20, 0, 20);
544 assert!(response.has_more);
545
546 let response = PaginationResponse::new(100, 20, 80, 20);
547 assert!(!response.has_more);
548 }
549
550 #[test]
551 fn test_error_code_http_status() {
552 assert_eq!(ErrorCode::ValidationError.http_status(), 400);
553 assert_eq!(ErrorCode::Unauthorized.http_status(), 401);
554 assert_eq!(ErrorCode::Forbidden.http_status(), 403);
555 assert_eq!(ErrorCode::NotFound.http_status(), 404);
556 assert_eq!(ErrorCode::Conflict.http_status(), 409);
557 assert_eq!(ErrorCode::RateLimited.http_status(), 429);
558 assert_eq!(ErrorCode::InternalError.http_status(), 500);
559 }
560
561 #[test]
562 fn test_error_response_serialization() {
563 let error = ErrorResponse::validation_error("Invalid email format")
564 .with_field_errors(vec![FieldError::new("email", "must be a valid email address")]);
565
566 let json = serde_json::to_string(&error).unwrap();
567 assert!(json.contains("VALIDATION_ERROR"));
568 assert!(json.contains("email"));
569 }
570
571 #[test]
572 fn test_time_range_filter() {
573 let filter = TimeRangeFilter::new();
574 assert!(!filter.has_constraints());
575
576 let filter = filter.since(Utc::now());
577 assert!(filter.has_constraints());
578 }
579
580 #[test]
581 fn test_paginated_response() {
582 let items = vec!["a", "b", "c"];
583 let response = PaginatedResponse::new(items, 10, 0, 3);
584
585 assert_eq!(response.items.len(), 3);
586 assert_eq!(response.pagination.total, 10);
587 assert!(response.pagination.has_more);
588 }
589
590 #[test]
591 fn test_batch_response() {
592 let results = vec![
593 BatchItemResult::success(0, "item1"),
594 BatchItemResult::failure(
595 1,
596 ErrorResponse::validation_error("Invalid"),
597 ),
598 BatchItemResult::success(2, "item3"),
599 ];
600
601 let batch = BatchResponse::new(results);
602 assert_eq!(batch.total, 3);
603 assert_eq!(batch.successful, 2);
604 assert_eq!(batch.failed, 1);
605 assert!(!batch.all_successful());
606 }
607}