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(ErrorCode::NotFound, format!("{} not found", entity.into()))
310 }
311
312 pub fn conflict(message: impl Into<String>) -> Self {
314 Self::new(ErrorCode::Conflict, message)
315 }
316
317 pub fn internal_error() -> Self {
319 Self::new(ErrorCode::InternalError, "An internal error occurred")
320 }
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct SuccessResponse<T> {
326 pub data: T,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub meta: Option<ResponseMeta>,
332}
333
334impl<T> SuccessResponse<T> {
335 pub fn new(data: T) -> Self {
336 Self { data, meta: None }
337 }
338
339 pub fn with_meta(mut self, meta: ResponseMeta) -> Self {
340 self.meta = Some(meta);
341 self
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ResponseMeta {
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub processing_time_ms: Option<u64>,
351
352 #[serde(skip_serializing_if = "Option::is_none")]
354 pub api_version: Option<String>,
355
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub deprecation_warning: Option<String>,
359}
360
361impl ResponseMeta {
362 pub fn new() -> Self {
363 Self {
364 processing_time_ms: None,
365 api_version: None,
366 deprecation_warning: None,
367 }
368 }
369
370 pub fn with_processing_time(mut self, ms: u64) -> Self {
371 self.processing_time_ms = Some(ms);
372 self
373 }
374}
375
376impl Default for ResponseMeta {
377 fn default() -> Self {
378 Self::new()
379 }
380}
381
382#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct PaginatedResponse<T> {
385 pub items: Vec<T>,
387
388 pub pagination: PaginationResponse,
390}
391
392impl<T> PaginatedResponse<T> {
393 pub fn new(items: Vec<T>, total: usize, offset: usize, limit: usize) -> Self {
394 let count = items.len();
395 Self {
396 items,
397 pagination: PaginationResponse::new(total, count, offset, limit),
398 }
399 }
400
401 pub fn empty() -> Self {
402 Self {
403 items: Vec::new(),
404 pagination: PaginationResponse::new(0, 0, 0, 20),
405 }
406 }
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct BatchItemResult<T> {
412 pub index: usize,
414
415 pub success: bool,
417
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub data: Option<T>,
421
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub error: Option<ErrorResponse>,
425}
426
427impl<T> BatchItemResult<T> {
428 pub fn success(index: usize, data: T) -> Self {
429 Self {
430 index,
431 success: true,
432 data: Some(data),
433 error: None,
434 }
435 }
436
437 pub fn failure(index: usize, error: ErrorResponse) -> Self {
438 Self {
439 index,
440 success: false,
441 data: None,
442 error: Some(error),
443 }
444 }
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct BatchResponse<T> {
450 pub results: Vec<BatchItemResult<T>>,
452
453 pub total: usize,
455
456 pub successful: usize,
458
459 pub failed: usize,
461}
462
463impl<T> BatchResponse<T> {
464 pub fn new(results: Vec<BatchItemResult<T>>) -> Self {
465 let total = results.len();
466 let successful = results.iter().filter(|r| r.success).count();
467 let failed = total - successful;
468
469 Self {
470 results,
471 total,
472 successful,
473 failed,
474 }
475 }
476
477 pub fn all_successful(&self) -> bool {
479 self.failed == 0
480 }
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct HealthResponse {
486 pub status: HealthStatus,
488
489 pub version: String,
491
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub uptime_seconds: Option<u64>,
495
496 #[serde(skip_serializing_if = "Vec::is_empty", default)]
498 pub components: Vec<ComponentHealth>,
499}
500
501#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
503#[serde(rename_all = "snake_case")]
504pub enum HealthStatus {
505 Healthy,
506 Degraded,
507 Unhealthy,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct ComponentHealth {
513 pub name: String,
515
516 pub status: HealthStatus,
518
519 #[serde(skip_serializing_if = "Option::is_none")]
521 pub message: Option<String>,
522
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub response_time_ms: Option<u64>,
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
533 fn test_pagination_request_clamping() {
534 let pagination = PaginationRequest::new(200, 0);
535 assert_eq!(pagination.clamped_limit(), 100);
536 }
537
538 #[test]
539 fn test_pagination_response_has_more() {
540 let response = PaginationResponse::new(100, 20, 0, 20);
541 assert!(response.has_more);
542
543 let response = PaginationResponse::new(100, 20, 80, 20);
544 assert!(!response.has_more);
545 }
546
547 #[test]
548 fn test_error_code_http_status() {
549 assert_eq!(ErrorCode::ValidationError.http_status(), 400);
550 assert_eq!(ErrorCode::Unauthorized.http_status(), 401);
551 assert_eq!(ErrorCode::Forbidden.http_status(), 403);
552 assert_eq!(ErrorCode::NotFound.http_status(), 404);
553 assert_eq!(ErrorCode::Conflict.http_status(), 409);
554 assert_eq!(ErrorCode::RateLimited.http_status(), 429);
555 assert_eq!(ErrorCode::InternalError.http_status(), 500);
556 }
557
558 #[test]
559 fn test_error_response_serialization() {
560 let error =
561 ErrorResponse::validation_error("Invalid email format").with_field_errors(vec![
562 FieldError::new("email", "must be a valid email address"),
563 ]);
564
565 let json = serde_json::to_string(&error).unwrap();
566 assert!(json.contains("VALIDATION_ERROR"));
567 assert!(json.contains("email"));
568 }
569
570 #[test]
571 fn test_time_range_filter() {
572 let filter = TimeRangeFilter::new();
573 assert!(!filter.has_constraints());
574
575 let filter = filter.since(Utc::now());
576 assert!(filter.has_constraints());
577 }
578
579 #[test]
580 fn test_paginated_response() {
581 let items = vec!["a", "b", "c"];
582 let response = PaginatedResponse::new(items, 10, 0, 3);
583
584 assert_eq!(response.items.len(), 3);
585 assert_eq!(response.pagination.total, 10);
586 assert!(response.pagination.has_more);
587 }
588
589 #[test]
590 fn test_batch_response() {
591 let results = vec![
592 BatchItemResult::success(0, "item1"),
593 BatchItemResult::failure(1, ErrorResponse::validation_error("Invalid")),
594 BatchItemResult::success(2, "item3"),
595 ];
596
597 let batch = BatchResponse::new(results);
598 assert_eq!(batch.total, 3);
599 assert_eq!(batch.successful, 2);
600 assert_eq!(batch.failed, 1);
601 assert!(!batch.all_successful());
602 }
603}