cal_core/rest/
common.rs

1// File: cal-core/src/rest/common.rs
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5#[cfg(feature = "openapi")]
6use utoipa::ToSchema;
7
8// Pagination
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct PaginationParams {
12    #[serde(default)]
13    pub page: u32,
14    #[serde(default = "default_page_size")]
15    pub page_size: u32,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[cfg_attr(feature = "openapi", derive(ToSchema))]
20#[serde(rename_all = "camelCase")]
21pub struct PaginatedResponse<T> {
22    pub data: Vec<T>,
23    pub pagination: PaginationMeta,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct PaginationMeta {
29    pub total: u64,
30    pub page: u32,
31    pub page_size: u32,
32    pub total_pages: u32,
33    pub has_next: bool,
34    pub has_previous: bool,
35}
36
37// Sorting
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "lowercase")]
40pub enum SortOrder {
41    Asc,
42    Desc,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct SortParams {
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub sort_by: Option<String>,
50    #[serde(default = "default_sort_order")]
51    pub sort_order: SortOrder,
52}
53
54// Time range filtering
55#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct TimeRange {
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub start: Option<chrono::DateTime<chrono::Utc>>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub end: Option<chrono::DateTime<chrono::Utc>>,
62}
63
64// Common error types
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[cfg_attr(feature = "openapi", derive(ToSchema))]
67#[serde(rename_all = "camelCase")]
68pub struct ApiError {
69    pub code: String,
70    pub message: String,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub details: Option<ErrorDetails>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub request_id: Option<String>,
75    pub timestamp: chrono::DateTime<chrono::Utc>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79#[cfg_attr(feature = "openapi", derive(ToSchema))]
80#[serde(rename_all = "camelCase")]
81pub struct ErrorDetails {
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub field: Option<String>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub reason: Option<String>,
86    #[serde(skip_serializing_if = "Vec::is_empty")]
87    pub validation_errors: Vec<ValidationError>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[cfg_attr(feature = "openapi", derive(ToSchema))]
92#[serde(rename_all = "camelCase")]
93pub struct ValidationError {
94    pub field: String,
95    pub code: String,
96    pub message: String,
97}
98
99// Batch operation results
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[cfg_attr(feature = "openapi", derive(ToSchema))]
102#[serde(rename_all = "camelCase")]
103pub struct BatchResult<T, E> {
104    pub successful: Vec<T>,
105    pub failed: Vec<BatchError<E>>,
106    pub total: usize,
107    pub success_count: usize,
108    pub failure_count: usize,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[cfg_attr(feature = "openapi", derive(ToSchema))]
113#[serde(rename_all = "camelCase")]
114pub struct BatchError<E> {
115    pub index: usize,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub id: Option<String>,
118    pub error: E,
119}
120
121// Common response wrapper
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[cfg_attr(feature = "openapi", derive(ToSchema))]
124#[serde(rename_all = "camelCase")]
125pub struct ApiResponse<T> {
126    pub success: bool,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub data: Option<T>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub error: Option<ApiError>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub metadata: Option<ResponseMetadata>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct ResponseMetadata {
138    pub request_id: String,
139    pub timestamp: chrono::DateTime<chrono::Utc>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub version: Option<String>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub processing_time_ms: Option<u64>,
144}
145
146// Search and filter
147#[derive(Debug, Clone, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct SearchParams {
150    pub query: String,
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub fields: Option<Vec<String>>,
153    #[serde(default)]
154    pub fuzzy: bool,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub highlight: Option<bool>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct FilterParams {
162    #[serde(flatten)]
163    pub filters: serde_json::Map<String, serde_json::Value>,
164}
165
166// ID wrapper for consistent ID handling
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
168#[cfg_attr(feature = "openapi", derive(ToSchema))]
169pub struct Id(pub String);
170
171// Common list request combining pagination, sorting, and filtering
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(rename_all = "camelCase")]
174pub struct ListRequest {
175    #[serde(flatten)]
176    pub pagination: PaginationParams,
177    #[serde(flatten)]
178    pub sort: SortParams,
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub search: Option<SearchParams>,
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub time_range: Option<TimeRange>,
183    #[serde(skip_serializing_if = "Option::is_none")]
184    pub filters: Option<FilterParams>,
185}
186
187// Helper functions
188fn default_page_size() -> u32 {
189    20
190}
191
192fn default_sort_order() -> SortOrder {
193    SortOrder::Asc
194}
195
196// Implementations
197impl Default for PaginationParams {
198    fn default() -> Self {
199        Self {
200            page: 0,
201            page_size: default_page_size(),
202        }
203    }
204}
205
206impl PaginationParams {
207    pub fn new(page: u32, page_size: u32) -> Self {
208        Self { page, page_size }
209    }
210
211    pub fn offset(&self) -> u64 {
212        (self.page as u64) * (self.page_size as u64)
213    }
214
215    pub fn limit(&self) -> u64 {
216        self.page_size as u64
217    }
218}
219
220impl PaginationMeta {
221    pub fn new(total: u64, page: u32, page_size: u32) -> Self {
222        let total_pages = ((total as f64) / (page_size as f64)).ceil() as u32;
223        Self {
224            total,
225            page,
226            page_size,
227            total_pages,
228            has_next: page < total_pages.saturating_sub(1),
229            has_previous: page > 0,
230        }
231    }
232}
233
234impl<T> PaginatedResponse<T> {
235    pub fn new(data: Vec<T>, total: u64, page: u32, page_size: u32) -> Self {
236        Self {
237            data,
238            pagination: PaginationMeta::new(total, page, page_size),
239        }
240    }
241}
242
243impl Default for SortOrder {
244    fn default() -> Self {
245        Self::Asc
246    }
247}
248
249impl fmt::Display for SortOrder {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        match self {
252            SortOrder::Asc => write!(f, "asc"),
253            SortOrder::Desc => write!(f, "desc"),
254        }
255    }
256}
257
258impl Default for SortParams {
259    fn default() -> Self {
260        Self {
261            sort_by: None,
262            sort_order: default_sort_order(),
263        }
264    }
265}
266
267impl ApiError {
268    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
269        Self {
270            code: code.into(),
271            message: message.into(),
272            details: None,
273            request_id: None,
274            timestamp: chrono::Utc::now(),
275        }
276    }
277
278    pub fn with_field(mut self, field: impl Into<String>) -> Self {
279        self.details = Some(ErrorDetails {
280            field: Some(field.into()),
281            reason: None,
282            validation_errors: Vec::new(),
283        });
284        self
285    }
286
287    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
288        self.request_id = Some(request_id.into());
289        self
290    }
291}
292
293impl<T> ApiResponse<T> {
294    pub fn success(data: T) -> Self {
295        Self {
296            success: true,
297            data: Some(data),
298            error: None,
299            metadata: None,
300        }
301    }
302
303    pub fn error(error: ApiError) -> Self {
304        Self {
305            success: false,
306            data: None,
307            error: Some(error),
308            metadata: None,
309        }
310    }
311
312    pub fn with_metadata(mut self, metadata: ResponseMetadata) -> Self {
313        self.metadata = Some(metadata);
314        self
315    }
316}
317
318impl<T, E> BatchResult<T, E> {
319    pub fn new(successful: Vec<T>, failed: Vec<BatchError<E>>) -> Self {
320        let success_count = successful.len();
321        let failure_count = failed.len();
322        Self {
323            successful,
324            failed,
325            total: success_count + failure_count,
326            success_count,
327            failure_count,
328        }
329    }
330
331    pub fn all_succeeded(&self) -> bool {
332        self.failure_count == 0
333    }
334
335    pub fn all_failed(&self) -> bool {
336        self.success_count == 0
337    }
338}
339
340impl fmt::Display for Id {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        write!(f, "{}", self.0)
343    }
344}
345
346impl From<String> for Id {
347    fn from(s: String) -> Self {
348        Id(s)
349    }
350}
351
352impl From<&str> for Id {
353    fn from(s: &str) -> Self {
354        Id(s.to_string())
355    }
356}
357
358impl TimeRange {
359    pub fn new(start: Option<chrono::DateTime<chrono::Utc>>, end: Option<chrono::DateTime<chrono::Utc>>) -> Self {
360        Self { start, end }
361    }
362
363    pub fn is_valid(&self) -> bool {
364        match (self.start, self.end) {
365            (Some(start), Some(end)) => start <= end,
366            _ => true,
367        }
368    }
369}
370
371// Re-export common types
372pub use chrono::{DateTime, Utc};