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(
310            ErrorCode::NotFound,
311            format!("{} not found", entity.into()),
312        )
313    }
314
315    /// Create a conflict error response
316    pub fn conflict(message: impl Into<String>) -> Self {
317        Self::new(ErrorCode::Conflict, message)
318    }
319
320    /// Create an internal error response
321    pub fn internal_error() -> Self {
322        Self::new(ErrorCode::InternalError, "An internal error occurred")
323    }
324}
325
326/// Success response wrapper with optional metadata
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct SuccessResponse<T> {
329    /// The response data
330    pub data: T,
331
332    /// Optional metadata about the response
333    #[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/// Response metadata
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct ResponseMeta {
351    /// Request processing time in milliseconds
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub processing_time_ms: Option<u64>,
354
355    /// API version
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub api_version: Option<String>,
358
359    /// Deprecation warning if applicable
360    #[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/// Paginated list response wrapper
386#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct PaginatedResponse<T> {
388    /// The list of items
389    pub items: Vec<T>,
390
391    /// Pagination metadata
392    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/// Batch operation result for individual items
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct BatchItemResult<T> {
415    /// Index of the item in the original batch
416    pub index: usize,
417
418    /// Whether this item was processed successfully
419    pub success: bool,
420
421    /// The result data if successful
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub data: Option<T>,
424
425    /// Error information if failed
426    #[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/// Batch operation response
451#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct BatchResponse<T> {
453    /// Results for each item in the batch
454    pub results: Vec<BatchItemResult<T>>,
455
456    /// Total items in the batch
457    pub total: usize,
458
459    /// Number of successful operations
460    pub successful: usize,
461
462    /// Number of failed operations
463    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    /// Check if all operations succeeded
481    pub fn all_successful(&self) -> bool {
482        self.failed == 0
483    }
484}
485
486/// Health check response
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct HealthResponse {
489    /// Overall health status
490    pub status: HealthStatus,
491
492    /// Service version
493    pub version: String,
494
495    /// Uptime in seconds
496    #[serde(skip_serializing_if = "Option::is_none")]
497    pub uptime_seconds: Option<u64>,
498
499    /// Component health checks
500    #[serde(skip_serializing_if = "Vec::is_empty", default)]
501    pub components: Vec<ComponentHealth>,
502}
503
504/// Health status
505#[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/// Component health status
514#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct ComponentHealth {
516    /// Component name
517    pub name: String,
518
519    /// Component status
520    pub status: HealthStatus,
521
522    /// Optional message
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub message: Option<String>,
525
526    /// Response time in milliseconds
527    #[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}