cal_core/rest/
common.rs

1// File: cal-core/src/rest/common.rs
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::fmt;
6#[cfg(feature = "openapi")]
7use utoipa::ToSchema;
8
9/// Response for synchronization operations
10#[derive(Debug, Serialize, Deserialize)]
11#[cfg_attr(feature = "openapi", derive(ToSchema))]
12#[cfg_attr(feature = "openapi", schema(
13    title ="Response returned from synchronization endpoints",
14    example = json!({
15        "status": "success",
16        "message": "Synchronization started successfully"
17    })
18))]
19pub struct SyncResponse {
20    /// Status of the sync operation (e.g., "success", "failed", "pending")
21    #[cfg_attr(feature = "openapi", schema(example = "success"))]
22    pub status: String,
23
24    /// Descriptive message about the sync operation
25    #[cfg_attr(feature = "openapi", schema(example = "Contact sync started in background"))]
26    pub message: String,
27}
28
29/// Response from cache operations
30#[derive(Debug, Serialize, Deserialize)]
31#[cfg_attr(feature = "openapi", derive(ToSchema))]
32#[cfg_attr(feature = "openapi", schema(
33    title ="Response containing data retrieved from cache"
34))]
35pub struct CacheResponse {
36    /// Source of the data (typically "cache")
37    #[cfg_attr(feature = "openapi", schema(example = "cache"))]
38    pub source: String,
39
40    /// The cached data as a JSON value
41    pub data: serde_json::Value,
42
43    /// Optional count of items in the data
44    #[serde(skip_serializing_if = "Option::is_none")]
45    #[cfg_attr(feature = "openapi", schema(example = 42))]
46    pub count: Option<usize>,
47
48    /// Additional information about the cached data
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub additional_info: Option<serde_json::Value>,
51}
52
53/// Error response from cache operations
54#[derive(Debug, Serialize, Deserialize)]
55#[cfg_attr(feature = "openapi", derive(ToSchema))]
56#[cfg_attr(feature = "openapi", schema(
57    title ="Error response when cache operation fails"
58))]
59pub struct CacheErrorResponse {
60    /// Error message describing what went wrong
61    #[cfg_attr(feature = "openapi", schema(example = "Redis connection timeout"))]
62    pub error: String,
63
64    /// Source of the error (typically "cache")
65    #[cfg_attr(feature = "openapi", schema(example = "cache"))]
66    pub source: String,
67}
68
69/// Cache statistics response
70#[derive(Debug, Serialize, Deserialize)]
71#[cfg_attr(feature = "openapi", derive(ToSchema))]
72#[cfg_attr(feature = "openapi", schema(
73    title ="Statistics about cached data for an account",
74    example = json!({
75        "source": "cache",
76        "account_id": "507f1f77bcf86cd799439011",
77        "stats": {
78            "summary": {
79                "total_contacts": 150
80            },
81            "groups": {
82                "sales": 50,
83                "support": 30,
84                "engineering": 70
85            }
86        }
87    })
88))]
89pub struct CacheStatsResponse {
90    /// Source of the statistics (typically "cache")
91    pub source: String,
92
93    /// Account ID these statistics belong to
94    #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
95    pub account_id: String,
96
97    /// Nested map of statistics categories and their values
98    pub stats: BTreeMap<String, BTreeMap<String, usize>>,
99}
100
101/// Pagination parameters for list requests
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[cfg_attr(feature = "openapi", derive(ToSchema))]
104#[cfg_attr(feature = "openapi", schema(title ="Pagination parameters for list requests"))]
105#[serde(rename_all = "camelCase")]
106pub struct PaginationParams {
107    /// Page number (0-based)
108    #[serde(default)]
109    #[cfg_attr(feature = "openapi", schema(example = 0, minimum = 0))]
110    pub page: u32,
111
112    /// Number of items per page
113    #[serde(default = "default_page_size")]
114    #[cfg_attr(feature = "openapi", schema(example = 20, minimum = 1, maximum = 100))]
115    pub page_size: u32,
116}
117
118/// Paginated response wrapper
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[cfg_attr(feature = "openapi", derive(ToSchema))]
121#[cfg_attr(feature = "openapi", schema(title ="Paginated list response"))]
122#[serde(rename_all = "camelCase")]
123pub struct PaginatedResponse<T> {
124    /// Array of items for the current page
125    pub data: Vec<T>,
126
127    /// Pagination metadata
128    pub pagination: PaginationMeta,
129}
130
131/// Pagination metadata
132#[derive(Debug, Clone, Serialize, Deserialize)]
133#[cfg_attr(feature = "openapi", derive(ToSchema))]
134#[cfg_attr(feature = "openapi", schema(
135    title ="Metadata about pagination",
136    example = json!({
137        "total": 150,
138        "page": 0,
139        "pageSize": 20,
140        "totalPages": 8,
141        "hasNext": true,
142        "hasPrevious": false
143    })
144))]
145#[serde(rename_all = "camelCase")]
146pub struct PaginationMeta {
147    /// Total number of items across all pages
148    pub total: u64,
149
150    /// Current page number (0-based)
151    pub page: u32,
152
153    /// Number of items per page
154    pub page_size: u32,
155
156    /// Total number of pages
157    pub total_pages: u32,
158
159    /// Whether there is a next page
160    pub has_next: bool,
161
162    /// Whether there is a previous page
163    pub has_previous: bool,
164}
165
166/// Sort order enumeration
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168#[cfg_attr(feature = "openapi", derive(ToSchema))]
169#[cfg_attr(feature = "openapi", schema(title ="Sort order direction"))]
170#[serde(rename_all = "lowercase")]
171pub enum SortOrder {
172    /// Ascending order
173    #[cfg_attr(feature = "openapi", schema(rename = "asc"))]
174    Asc,
175
176    /// Descending order
177    #[cfg_attr(feature = "openapi", schema(rename = "desc"))]
178    Desc,
179}
180
181/// Sorting parameters
182#[derive(Debug, Clone, Serialize, Deserialize)]
183#[cfg_attr(feature = "openapi", derive(ToSchema))]
184#[cfg_attr(feature = "openapi", schema(title ="Sorting parameters for list requests"))]
185#[serde(rename_all = "camelCase")]
186pub struct SortParams {
187    /// Field to sort by
188    #[serde(skip_serializing_if = "Option::is_none")]
189    #[cfg_attr(feature = "openapi", schema(example = "name"))]
190    pub sort_by: Option<String>,
191
192    /// Sort order (asc or desc)
193    #[serde(default = "default_sort_order")]
194    pub sort_order: SortOrder,
195}
196
197/// Time range filter
198#[derive(Debug, Clone, Serialize, Deserialize)]
199#[cfg_attr(feature = "openapi", derive(ToSchema))]
200#[cfg_attr(feature = "openapi", schema(title ="Time range filter for queries"))]
201#[serde(rename_all = "camelCase")]
202pub struct TimeRange {
203    /// Start time (inclusive)
204    #[serde(skip_serializing_if = "Option::is_none")]
205    #[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime, example = "2024-01-01T00:00:00Z"))]
206    pub start: Option<chrono::DateTime<chrono::Utc>>,
207
208    /// End time (inclusive)
209    #[serde(skip_serializing_if = "Option::is_none")]
210    #[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime, example = "2024-12-31T23:59:59Z"))]
211    pub end: Option<chrono::DateTime<chrono::Utc>>,
212}
213
214/// API error response
215#[derive(Debug, Clone, Serialize, Deserialize)]
216#[cfg_attr(feature = "openapi", derive(ToSchema))]
217#[cfg_attr(feature = "openapi", schema(
218    title ="Standard error response",
219    example = json!({
220        "code": "VALIDATION_ERROR",
221        "message": "Invalid input provided",
222        "timestamp": 1704067200
223    })
224))]
225#[serde(rename_all = "camelCase")]
226pub struct ApiError {
227    /// Error code for programmatic handling
228    #[cfg_attr(feature = "openapi", schema(example = "VALIDATION_ERROR"))]
229    pub code: String,
230
231    /// Human-readable error message
232    #[cfg_attr(feature = "openapi", schema(example = "Invalid email format"))]
233    pub message: String,
234
235    /// Additional error details
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub details: Option<ErrorDetails>,
238
239    /// Request ID for tracking
240    #[serde(skip_serializing_if = "Option::is_none")]
241    #[cfg_attr(feature = "openapi", schema(example = "req_1234567890"))]
242    pub request_id: Option<String>,
243
244    /// Unix timestamp when the error occurred
245    #[cfg_attr(feature = "openapi", schema(value_type = i64, example = 1704067200))]
246    pub timestamp: Option<i64>,
247}
248
249/// Detailed error information
250#[derive(Debug, Clone, Serialize, Deserialize)]
251#[cfg_attr(feature = "openapi", derive(ToSchema))]
252#[cfg_attr(feature = "openapi", schema(title ="Additional details about an error"))]
253#[serde(rename_all = "camelCase")]
254pub struct ErrorDetails {
255    /// Specific field that caused the error
256    #[serde(skip_serializing_if = "Option::is_none")]
257    #[cfg_attr(feature = "openapi", schema(example = "email"))]
258    pub field: Option<String>,
259
260    /// Reason for the error
261    #[serde(skip_serializing_if = "Option::is_none")]
262    #[cfg_attr(feature = "openapi", schema(example = "Invalid format"))]
263    pub reason: Option<String>,
264
265    /// List of validation errors
266    #[serde(skip_serializing_if = "Vec::is_empty")]
267    pub validation_errors: Vec<ValidationError>,
268}
269
270/// Validation error details
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[cfg_attr(feature = "openapi", derive(ToSchema))]
273#[cfg_attr(feature = "openapi", schema(title ="Details about a field validation error"))]
274#[serde(rename_all = "camelCase")]
275pub struct ValidationError {
276    /// Field that failed validation
277    #[cfg_attr(feature = "openapi", schema(example = "email"))]
278    pub field: String,
279
280    /// Validation error code
281    #[cfg_attr(feature = "openapi", schema(example = "format"))]
282    pub code: String,
283
284    /// Human-readable validation error message
285    #[cfg_attr(feature = "openapi", schema(example = "Email must be a valid email address"))]
286    pub message: String,
287}
288
289/// Result of a batch operation
290#[derive(Debug, Clone, Serialize, Deserialize)]
291#[cfg_attr(feature = "openapi", derive(ToSchema))]
292#[cfg_attr(feature = "openapi", schema(
293    title ="Result of a batch operation showing successes and failures"
294))]
295#[serde(rename_all = "camelCase")]
296pub struct BatchResult<T, E> {
297    /// Successfully processed items
298    pub successful: Vec<T>,
299
300    /// Failed items with error details
301    pub failed: Vec<BatchError<E>>,
302
303    /// Total number of items processed
304    #[cfg_attr(feature = "openapi", schema(example = 100))]
305    pub total: usize,
306
307    /// Number of successful items
308    #[cfg_attr(feature = "openapi", schema(example = 95))]
309    pub success_count: usize,
310
311    /// Number of failed items
312    #[cfg_attr(feature = "openapi", schema(example = 5))]
313    pub failure_count: usize,
314}
315
316/// Error information for a single item in a batch operation
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[cfg_attr(feature = "openapi", derive(ToSchema))]
319#[cfg_attr(feature = "openapi", schema(title ="Error details for a failed batch item"))]
320#[serde(rename_all = "camelCase")]
321pub struct BatchError<E> {
322    /// Index of the item in the original batch
323    #[cfg_attr(feature = "openapi", schema(example = 42))]
324    pub index: usize,
325
326    /// Optional ID of the item that failed
327    #[serde(skip_serializing_if = "Option::is_none")]
328    #[cfg_attr(feature = "openapi", schema(example = "507f1f77bcf86cd799439011"))]
329    pub id: Option<String>,
330
331    /// Error that occurred for this item
332    pub error: E,
333}
334
335/// Standard API response wrapper
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[cfg_attr(feature = "openapi", derive(ToSchema))]
338#[cfg_attr(feature = "openapi", schema(
339    title ="Standard API response wrapper with success/error handling"
340))]
341#[serde(rename_all = "camelCase")]
342pub struct ApiResponse<T> {
343    /// Whether the request was successful
344    #[cfg_attr(feature = "openapi", schema(example = true))]
345    pub success: bool,
346
347    /// Response data (present on success)
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub data: Option<T>,
350
351    /// Error information (present on failure)
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub error: Option<ApiError>,
354
355    /// Response metadata
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub metadata: Option<ResponseMetadata>,
358}
359
360/// Response metadata
361#[derive(Debug, Clone, Serialize, Deserialize)]
362#[cfg_attr(feature = "openapi", derive(ToSchema))]
363#[cfg_attr(feature = "openapi", schema(title ="Metadata about the API response"))]
364#[serde(rename_all = "camelCase")]
365pub struct ResponseMetadata {
366    /// Unique request identifier
367    #[cfg_attr(feature = "openapi", schema(example = "req_1234567890"))]
368    pub request_id: String,
369
370    /// Timestamp when the response was generated
371    #[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime, example = "2024-01-01T00:00:00Z"))]
372    pub timestamp: chrono::DateTime<chrono::Utc>,
373
374    /// API version
375    #[serde(skip_serializing_if = "Option::is_none")]
376    #[cfg_attr(feature = "openapi", schema(example = "1.0.0"))]
377    pub version: Option<String>,
378
379    /// Processing time in milliseconds
380    #[serde(skip_serializing_if = "Option::is_none")]
381    #[cfg_attr(feature = "openapi", schema(example = 42))]
382    pub processing_time_ms: Option<u64>,
383}
384
385/// Search parameters
386#[derive(Debug, Clone, Serialize, Deserialize)]
387#[cfg_attr(feature = "openapi", derive(ToSchema))]
388#[cfg_attr(feature = "openapi", schema(title ="Parameters for search queries"))]
389#[serde(rename_all = "camelCase")]
390pub struct SearchParams {
391    /// Search query string
392    #[cfg_attr(feature = "openapi", schema(example = "john doe"))]
393    pub query: String,
394
395    /// Fields to search in
396    #[serde(skip_serializing_if = "Option::is_none")]
397    #[cfg_attr(feature = "openapi", schema(example = json!(["name", "email"])))]
398    pub fields: Option<Vec<String>>,
399
400    /// Enable fuzzy search
401    #[serde(default)]
402    #[cfg_attr(feature = "openapi", schema(example = false))]
403    pub fuzzy: bool,
404
405    /// Enable search result highlighting
406    #[serde(skip_serializing_if = "Option::is_none")]
407    #[cfg_attr(feature = "openapi", schema(example = true))]
408    pub highlight: Option<bool>,
409}
410
411/// Filter parameters
412#[derive(Debug, Clone, Serialize, Deserialize)]
413#[cfg_attr(feature = "openapi", derive(ToSchema))]
414#[cfg_attr(feature = "openapi", schema(
415    title ="Dynamic filter parameters",
416    example = json!({
417        "status": "active",
418        "category": "premium"
419    })
420))]
421#[serde(rename_all = "camelCase")]
422pub struct FilterParams {
423    /// Dynamic filters as key-value pairs
424    #[serde(flatten)]
425    pub filters: serde_json::Map<String, serde_json::Value>,
426}
427
428/// Wrapper for consistent ID handling
429#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
430#[cfg_attr(feature = "openapi", derive(ToSchema))]
431#[cfg_attr(feature = "openapi", schema(
432    title ="ID wrapper for consistent handling",
433    value_type = String,
434    example = "507f1f77bcf86cd799439011"
435))]
436pub struct Id(pub String);
437
438/// Combined list request parameters
439#[derive(Debug, Clone, Serialize, Deserialize)]
440#[cfg_attr(feature = "openapi", derive(ToSchema))]
441#[cfg_attr(feature = "openapi", schema(title ="Common parameters for list endpoints"))]
442#[serde(rename_all = "camelCase")]
443pub struct ListRequest {
444    /// Pagination parameters
445    #[serde(flatten)]
446    pub pagination: PaginationParams,
447
448    /// Sorting parameters
449    #[serde(flatten)]
450    pub sort: SortParams,
451
452    /// Search parameters
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub search: Option<SearchParams>,
455
456    /// Time range filter
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub time_range: Option<TimeRange>,
459
460    /// Additional filters
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub filters: Option<FilterParams>,
463}
464
465// Helper functions
466fn default_page_size() -> u32 {
467    20
468}
469
470fn default_sort_order() -> SortOrder {
471    SortOrder::Asc
472}
473
474// Implementations
475impl Default for PaginationParams {
476    fn default() -> Self {
477        Self {
478            page: 0,
479            page_size: default_page_size(),
480        }
481    }
482}
483
484impl PaginationParams {
485    pub fn new(page: u32, page_size: u32) -> Self {
486        Self { page, page_size }
487    }
488
489    pub fn offset(&self) -> u64 {
490        (self.page as u64) * (self.page_size as u64)
491    }
492
493    pub fn limit(&self) -> u64 {
494        self.page_size as u64
495    }
496}
497
498impl PaginationMeta {
499    pub fn new(total: u64, page: u32, page_size: u32) -> Self {
500        let total_pages = ((total as f64) / (page_size as f64)).ceil() as u32;
501        Self {
502            total,
503            page,
504            page_size,
505            total_pages,
506            has_next: page < total_pages.saturating_sub(1),
507            has_previous: page > 0,
508        }
509    }
510}
511
512impl<T> PaginatedResponse<T> {
513    pub fn new(data: Vec<T>, total: u64, page: u32, page_size: u32) -> Self {
514        Self {
515            data,
516            pagination: PaginationMeta::new(total, page, page_size),
517        }
518    }
519}
520
521impl Default for SortOrder {
522    fn default() -> Self {
523        Self::Asc
524    }
525}
526
527impl fmt::Display for SortOrder {
528    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
529        match self {
530            SortOrder::Asc => write!(f, "asc"),
531            SortOrder::Desc => write!(f, "desc"),
532        }
533    }
534}
535
536impl Default for SortParams {
537    fn default() -> Self {
538        Self {
539            sort_by: None,
540            sort_order: default_sort_order(),
541        }
542    }
543}
544
545impl ApiError {
546    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
547        Self {
548            code: code.into(),
549            message: message.into(),
550            details: None,
551            request_id: None,
552            timestamp: Some(chrono::Utc::now().timestamp()),
553        }
554    }
555
556    pub fn with_field(mut self, field: impl Into<String>) -> Self {
557        self.details = Some(ErrorDetails {
558            field: Some(field.into()),
559            reason: None,
560            validation_errors: Vec::new(),
561        });
562        self
563    }
564
565    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
566        self.request_id = Some(request_id.into());
567        self
568    }
569}
570
571impl<T> ApiResponse<T> {
572    pub fn success(data: T) -> Self {
573        Self {
574            success: true,
575            data: Some(data),
576            error: None,
577            metadata: None,
578        }
579    }
580
581    pub fn error(error: ApiError) -> Self {
582        Self {
583            success: false,
584            data: None,
585            error: Some(error),
586            metadata: None,
587        }
588    }
589
590    pub fn with_metadata(mut self, metadata: ResponseMetadata) -> Self {
591        self.metadata = Some(metadata);
592        self
593    }
594}
595
596impl<T, E> BatchResult<T, E> {
597    pub fn new(successful: Vec<T>, failed: Vec<BatchError<E>>) -> Self {
598        let success_count = successful.len();
599        let failure_count = failed.len();
600        Self {
601            successful,
602            failed,
603            total: success_count + failure_count,
604            success_count,
605            failure_count,
606        }
607    }
608
609    pub fn all_succeeded(&self) -> bool {
610        self.failure_count == 0
611    }
612
613    pub fn all_failed(&self) -> bool {
614        self.success_count == 0
615    }
616}
617
618impl fmt::Display for Id {
619    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
620        write!(f, "{}", self.0)
621    }
622}
623
624impl From<String> for Id {
625    fn from(s: String) -> Self {
626        Id(s)
627    }
628}
629
630impl From<&str> for Id {
631    fn from(s: &str) -> Self {
632        Id(s.to_string())
633    }
634}
635
636impl TimeRange {
637    pub fn new(start: Option<chrono::DateTime<chrono::Utc>>, end: Option<chrono::DateTime<chrono::Utc>>) -> Self {
638        Self { start, end }
639    }
640
641    pub fn is_valid(&self) -> bool {
642        match (self.start, self.end) {
643            (Some(start), Some(end)) => start <= end,
644            _ => true,
645        }
646    }
647}