Skip to main content

allsource_core/application/dto/
common_dto.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4/// Pagination request parameters
5#[derive(Debug, Clone, Serialize, Deserialize, Default)]
6pub struct PaginationRequest {
7    /// Number of items to return (default: 20, max: 100)
8    #[serde(default = "default_limit")]
9    pub limit: usize,
10
11    /// Number of items to skip (default: 0)
12    #[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    /// Clamp limit to max 100
29    pub fn clamped_limit(&self) -> usize {
30        self.limit.min(100)
31    }
32}
33
34/// Pagination metadata in responses
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PaginationResponse {
37    /// Total number of items available
38    pub total: usize,
39
40    /// Number of items returned in this response
41    pub count: usize,
42
43    /// Current offset
44    pub offset: usize,
45
46    /// Limit used for this request
47    pub limit: usize,
48
49    /// Whether there are more items after this page
50    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/// Sort direction
66#[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/// Generic sort request
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct SortRequest {
77    /// Field to sort by
78    pub field: String,
79
80    /// Sort direction
81    #[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/// Time range filter for queries
103#[derive(Debug, Clone, Serialize, Deserialize, Default)]
104pub struct TimeRangeFilter {
105    /// Start of time range (inclusive)
106    pub since: Option<DateTime<Utc>>,
107
108    /// End of time range (inclusive)
109    pub until: Option<DateTime<Utc>>,
110
111    /// Point-in-time query (time-travel)
112    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    /// Check if the filter has any time constraints
136    pub fn has_constraints(&self) -> bool {
137        self.since.is_some() || self.until.is_some() || self.as_of.is_some()
138    }
139}
140
141/// Error severity levels
142#[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/// Error code categories
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
153pub enum ErrorCode {
154    // Validation errors (400)
155    ValidationError,
156    InvalidInput,
157    MissingField,
158    InvalidFormat,
159
160    // Authentication errors (401)
161    Unauthorized,
162    InvalidToken,
163    TokenExpired,
164
165    // Authorization errors (403)
166    Forbidden,
167    InsufficientPermissions,
168    QuotaExceeded,
169
170    // Not found errors (404)
171    NotFound,
172    EntityNotFound,
173    ResourceNotFound,
174
175    // Conflict errors (409)
176    Conflict,
177    DuplicateEntry,
178    ConcurrencyConflict,
179    VersionMismatch,
180
181    // Rate limiting (429)
182    RateLimited,
183    TooManyRequests,
184
185    // Server errors (500)
186    InternalError,
187    DatabaseError,
188    ServiceUnavailable,
189}
190
191impl ErrorCode {
192    /// Get the HTTP status code for this error code
193    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/// Field-level validation error
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct FieldError {
225    /// Field name that has the error
226    pub field: String,
227
228    /// Error message for this field
229    pub message: String,
230
231    /// Error code for this field error
232    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/// Standard error response DTO for API errors
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ErrorResponse {
253    /// Error code
254    pub code: ErrorCode,
255
256    /// Human-readable error message
257    pub message: String,
258
259    /// Additional details about the error
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub details: Option<String>,
262
263    /// Field-level validation errors
264    #[serde(skip_serializing_if = "Vec::is_empty", default)]
265    pub field_errors: Vec<FieldError>,
266
267    /// Request ID for tracing
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub request_id: Option<String>,
270
271    /// Timestamp of the error
272    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    /// Create a validation error response
303    pub fn validation_error(message: impl Into<String>) -> Self {
304        Self::new(ErrorCode::ValidationError, message)
305    }
306
307    /// Create a not found error response
308    pub fn not_found(entity: impl Into<String>) -> Self {
309        Self::new(ErrorCode::NotFound, format!("{} not found", entity.into()))
310    }
311
312    /// Create a conflict error response
313    pub fn conflict(message: impl Into<String>) -> Self {
314        Self::new(ErrorCode::Conflict, message)
315    }
316
317    /// Create an internal error response
318    pub fn internal_error() -> Self {
319        Self::new(ErrorCode::InternalError, "An internal error occurred")
320    }
321}
322
323/// Success response wrapper with optional metadata
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct SuccessResponse<T> {
326    /// The response data
327    pub data: T,
328
329    /// Optional metadata about the response
330    #[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/// Response metadata
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ResponseMeta {
348    /// Request processing time in milliseconds
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub processing_time_ms: Option<u64>,
351
352    /// API version
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub api_version: Option<String>,
355
356    /// Deprecation warning if applicable
357    #[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/// Paginated list response wrapper
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct PaginatedResponse<T> {
385    /// The list of items
386    pub items: Vec<T>,
387
388    /// Pagination metadata
389    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/// Batch operation result for individual items
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct BatchItemResult<T> {
412    /// Index of the item in the original batch
413    pub index: usize,
414
415    /// Whether this item was processed successfully
416    pub success: bool,
417
418    /// The result data if successful
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub data: Option<T>,
421
422    /// Error information if failed
423    #[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/// Batch operation response
448#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct BatchResponse<T> {
450    /// Results for each item in the batch
451    pub results: Vec<BatchItemResult<T>>,
452
453    /// Total items in the batch
454    pub total: usize,
455
456    /// Number of successful operations
457    pub successful: usize,
458
459    /// Number of failed operations
460    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    /// Check if all operations succeeded
478    pub fn all_successful(&self) -> bool {
479        self.failed == 0
480    }
481}
482
483/// Health check response
484#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct HealthResponse {
486    /// Overall health status
487    pub status: HealthStatus,
488
489    /// Service version
490    pub version: String,
491
492    /// Uptime in seconds
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub uptime_seconds: Option<u64>,
495
496    /// Component health checks
497    #[serde(skip_serializing_if = "Vec::is_empty", default)]
498    pub components: Vec<ComponentHealth>,
499}
500
501/// Health status
502#[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/// Component health status
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct ComponentHealth {
513    /// Component name
514    pub name: String,
515
516    /// Component status
517    pub status: HealthStatus,
518
519    /// Optional message
520    #[serde(skip_serializing_if = "Option::is_none")]
521    pub message: Option<String>,
522
523    /// Response time in milliseconds
524    #[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}