llm_analytics_hub/models/
api.rs

1//! API Response Models
2//!
3//! Query result formats, pagination structures, error response schemas,
4//! and streaming data formats for the analytics hub API.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11// ============================================================================
12// STANDARD API RESPONSE WRAPPER
13// ============================================================================
14
15/// Standard API response wrapper
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ApiResponse<T> {
18    /// Response status
19    pub status: ResponseStatus,
20
21    /// Response data (present on success)
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub data: Option<T>,
24
25    /// Error details (present on error)
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub error: Option<ApiError>,
28
29    /// Response metadata
30    pub meta: ResponseMetadata,
31}
32
33impl<T> ApiResponse<T> {
34    pub fn success(data: T) -> Self {
35        Self {
36            status: ResponseStatus::Success,
37            data: Some(data),
38            error: None,
39            meta: ResponseMetadata::default(),
40        }
41    }
42
43    pub fn error(error: ApiError) -> Self {
44        Self {
45            status: ResponseStatus::Error,
46            data: None,
47            error: Some(error),
48            meta: ResponseMetadata::default(),
49        }
50    }
51
52    pub fn with_meta(mut self, meta: ResponseMetadata) -> Self {
53        self.meta = meta;
54        self
55    }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "lowercase")]
60pub enum ResponseStatus {
61    Success,
62    Error,
63    Partial,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ResponseMetadata {
68    /// Request identifier for tracing
69    pub request_id: Uuid,
70
71    /// Server timestamp
72    pub timestamp: DateTime<Utc>,
73
74    /// API version
75    pub api_version: String,
76
77    /// Response time in milliseconds
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub response_time_ms: Option<u64>,
80
81    /// Additional metadata
82    #[serde(flatten)]
83    pub extra: HashMap<String, serde_json::Value>,
84}
85
86impl Default for ResponseMetadata {
87    fn default() -> Self {
88        Self {
89            request_id: Uuid::new_v4(),
90            timestamp: Utc::now(),
91            api_version: "1.0.0".to_string(),
92            response_time_ms: None,
93            extra: HashMap::new(),
94        }
95    }
96}
97
98// ============================================================================
99// PAGINATION
100// ============================================================================
101
102/// Paginated response wrapper
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct PaginatedResponse<T> {
105    /// Response status
106    pub status: ResponseStatus,
107
108    /// Page data
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub data: Option<Vec<T>>,
111
112    /// Pagination metadata
113    pub pagination: PaginationMetadata,
114
115    /// Error details (present on error)
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub error: Option<ApiError>,
118
119    /// Response metadata
120    pub meta: ResponseMetadata,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct PaginationMetadata {
125    /// Current page number (1-indexed)
126    pub page: u32,
127
128    /// Items per page
129    pub per_page: u32,
130
131    /// Total number of items
132    pub total_items: u64,
133
134    /// Total number of pages
135    pub total_pages: u32,
136
137    /// Whether there is a next page
138    pub has_next: bool,
139
140    /// Whether there is a previous page
141    pub has_previous: bool,
142
143    /// Links to related pages
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub links: Option<PaginationLinks>,
146}
147
148impl PaginationMetadata {
149    pub fn new(page: u32, per_page: u32, total_items: u64) -> Self {
150        let total_pages = ((total_items as f64) / (per_page as f64)).ceil() as u32;
151        Self {
152            page,
153            per_page,
154            total_items,
155            total_pages,
156            has_next: page < total_pages,
157            has_previous: page > 1,
158            links: None,
159        }
160    }
161
162    pub fn with_links(mut self, base_url: &str) -> Self {
163        self.links = Some(PaginationLinks::new(base_url, self.page, self.total_pages));
164        self
165    }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct PaginationLinks {
170    /// Link to first page
171    pub first: String,
172
173    /// Link to last page
174    pub last: String,
175
176    /// Link to next page
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub next: Option<String>,
179
180    /// Link to previous page
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub prev: Option<String>,
183
184    /// Link to current page
185    pub self_link: String,
186}
187
188impl PaginationLinks {
189    pub fn new(base_url: &str, current_page: u32, total_pages: u32) -> Self {
190        Self {
191            first: format!("{}?page=1", base_url),
192            last: format!("{}?page={}", base_url, total_pages),
193            next: if current_page < total_pages {
194                Some(format!("{}?page={}", base_url, current_page + 1))
195            } else {
196                None
197            },
198            prev: if current_page > 1 {
199                Some(format!("{}?page={}", base_url, current_page - 1))
200            } else {
201                None
202            },
203            self_link: format!("{}?page={}", base_url, current_page),
204        }
205    }
206}
207
208/// Pagination parameters for requests
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct PaginationParams {
211    /// Page number (1-indexed)
212    #[serde(default = "default_page")]
213    pub page: u32,
214
215    /// Items per page
216    #[serde(default = "default_per_page")]
217    pub per_page: u32,
218
219    /// Sort field
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub sort_by: Option<String>,
222
223    /// Sort order
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub sort_order: Option<SortOrder>,
226}
227
228fn default_page() -> u32 {
229    1
230}
231
232fn default_per_page() -> u32 {
233    50
234}
235
236impl Default for PaginationParams {
237    fn default() -> Self {
238        Self {
239            page: 1,
240            per_page: 50,
241            sort_by: None,
242            sort_order: None,
243        }
244    }
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "lowercase")]
249pub enum SortOrder {
250    Asc,
251    Desc,
252}
253
254// ============================================================================
255// ERROR RESPONSES
256// ============================================================================
257
258/// API error response
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct ApiError {
261    /// Error code (machine-readable)
262    pub code: String,
263
264    /// Error message (human-readable)
265    pub message: String,
266
267    /// HTTP status code
268    pub status_code: u16,
269
270    /// Detailed error information
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub details: Option<ErrorDetails>,
273
274    /// Field-specific errors (for validation errors)
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub field_errors: Option<HashMap<String, Vec<String>>>,
277
278    /// Timestamp when error occurred
279    pub timestamp: DateTime<Utc>,
280}
281
282impl ApiError {
283    pub fn new(code: impl Into<String>, message: impl Into<String>, status_code: u16) -> Self {
284        Self {
285            code: code.into(),
286            message: message.into(),
287            status_code,
288            details: None,
289            field_errors: None,
290            timestamp: Utc::now(),
291        }
292    }
293
294    pub fn bad_request(message: impl Into<String>) -> Self {
295        Self::new("bad_request", message, 400)
296    }
297
298    pub fn unauthorized(message: impl Into<String>) -> Self {
299        Self::new("unauthorized", message, 401)
300    }
301
302    pub fn forbidden(message: impl Into<String>) -> Self {
303        Self::new("forbidden", message, 403)
304    }
305
306    pub fn not_found(message: impl Into<String>) -> Self {
307        Self::new("not_found", message, 404)
308    }
309
310    pub fn internal_error(message: impl Into<String>) -> Self {
311        Self::new("internal_error", message, 500)
312    }
313
314    pub fn with_details(mut self, details: ErrorDetails) -> Self {
315        self.details = Some(details);
316        self
317    }
318
319    pub fn with_field_errors(mut self, errors: HashMap<String, Vec<String>>) -> Self {
320        self.field_errors = Some(errors);
321        self
322    }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct ErrorDetails {
327    /// Error trace/stack trace (for debugging)
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub trace: Option<String>,
330
331    /// Additional context
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub context: Option<HashMap<String, serde_json::Value>>,
334
335    /// Suggested fixes or actions
336    #[serde(skip_serializing_if = "Option::is_none")]
337    pub suggestions: Option<Vec<String>>,
338
339    /// Documentation link
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub documentation_url: Option<String>,
342}
343
344// ============================================================================
345// QUERY RESULT FORMATS
346// ============================================================================
347
348/// Generic query result
349#[derive(Debug, Clone, Serialize, Deserialize)]
350pub struct QueryResult<T> {
351    /// Query identifier
352    pub query_id: Uuid,
353
354    /// Query execution status
355    pub status: QueryStatus,
356
357    /// Result data
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub data: Option<T>,
360
361    /// Query execution metrics
362    pub metrics: QueryMetrics,
363
364    /// Warnings encountered during query execution
365    #[serde(default)]
366    pub warnings: Vec<String>,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
370#[serde(rename_all = "snake_case")]
371pub enum QueryStatus {
372    Success,
373    PartialSuccess,
374    Failed,
375    Timeout,
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct QueryMetrics {
380    /// Query execution time in milliseconds
381    pub execution_time_ms: u64,
382
383    /// Number of records scanned
384    pub records_scanned: u64,
385
386    /// Number of records returned
387    pub records_returned: u64,
388
389    /// Data processed in bytes
390    pub bytes_processed: u64,
391
392    /// Whether results were cached
393    pub from_cache: bool,
394
395    /// Cache TTL if cached (seconds)
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub cache_ttl: Option<u32>,
398}
399
400/// Time-series query result
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct TimeSeriesQueryResult {
403    /// Query specification
404    pub query: String,
405
406    /// Time range
407    pub time_range: TimeRange,
408
409    /// Result series
410    pub series: Vec<SeriesData>,
411
412    /// Query metrics
413    pub metrics: QueryMetrics,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct TimeRange {
418    pub start: DateTime<Utc>,
419    pub end: DateTime<Utc>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct SeriesData {
424    /// Series name
425    pub name: String,
426
427    /// Series tags
428    #[serde(default)]
429    pub tags: HashMap<String, String>,
430
431    /// Data points
432    pub points: Vec<DataPoint>,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct DataPoint {
437    /// Timestamp
438    pub timestamp: DateTime<Utc>,
439
440    /// Value
441    pub value: f64,
442
443    /// Additional fields
444    #[serde(flatten)]
445    pub fields: HashMap<String, serde_json::Value>,
446}
447
448/// Aggregated metrics query result
449#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct MetricsQueryResult {
451    /// Metric name
452    pub metric: String,
453
454    /// Aggregation window
455    pub window: String,
456
457    /// Aggregated values
458    pub values: Vec<AggregatedValue>,
459
460    /// Query metrics
461    pub metrics: QueryMetrics,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct AggregatedValue {
466    pub timestamp: DateTime<Utc>,
467    pub avg: f64,
468    pub min: f64,
469    pub max: f64,
470    pub p50: f64,
471    pub p95: f64,
472    pub p99: f64,
473    pub count: u64,
474}
475
476// ============================================================================
477// STREAMING RESPONSE FORMATS
478// ============================================================================
479
480/// Streaming event wrapper
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct StreamEvent<T> {
483    /// Event identifier
484    pub event_id: Uuid,
485
486    /// Event type
487    pub event_type: StreamEventType,
488
489    /// Event data
490    pub data: T,
491
492    /// Sequence number (for ordering)
493    pub sequence: u64,
494
495    /// Timestamp
496    pub timestamp: DateTime<Utc>,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
500#[serde(rename_all = "snake_case")]
501pub enum StreamEventType {
502    Data,
503    Heartbeat,
504    Error,
505    Complete,
506}
507
508/// Server-Sent Events (SSE) format
509#[derive(Debug, Clone)]
510pub struct SseMessage {
511    /// Event type
512    pub event: Option<String>,
513
514    /// Data payload
515    pub data: String,
516
517    /// Event ID (for reconnection)
518    pub id: Option<String>,
519
520    /// Retry interval in milliseconds
521    pub retry: Option<u32>,
522}
523
524impl SseMessage {
525    pub fn data(data: String) -> Self {
526        Self {
527            event: None,
528            data,
529            id: None,
530            retry: None,
531        }
532    }
533
534    pub fn event(event: String, data: String) -> Self {
535        Self {
536            event: Some(event),
537            data,
538            id: None,
539            retry: None,
540        }
541    }
542
543    pub fn with_id(mut self, id: String) -> Self {
544        self.id = Some(id);
545        self
546    }
547
548    pub fn to_string(&self) -> String {
549        let mut result = String::new();
550
551        if let Some(event) = &self.event {
552            result.push_str(&format!("event: {}\n", event));
553        }
554
555        if let Some(id) = &self.id {
556            result.push_str(&format!("id: {}\n", id));
557        }
558
559        if let Some(retry) = &self.retry {
560            result.push_str(&format!("retry: {}\n", retry));
561        }
562
563        result.push_str(&format!("data: {}\n\n", self.data));
564        result
565    }
566}
567
568/// Batch response for bulk operations
569#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct BatchResponse<T> {
571    /// Batch identifier
572    pub batch_id: Uuid,
573
574    /// Total items in batch
575    pub total_items: usize,
576
577    /// Successfully processed items
578    pub success_count: usize,
579
580    /// Failed items
581    pub failure_count: usize,
582
583    /// Results for each item
584    pub results: Vec<BatchItemResult<T>>,
585
586    /// Overall batch status
587    pub status: BatchStatus,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct BatchItemResult<T> {
592    /// Item index in the batch
593    pub index: usize,
594
595    /// Item identifier
596    pub item_id: Option<String>,
597
598    /// Processing status
599    pub status: ItemStatus,
600
601    /// Result data (on success)
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub data: Option<T>,
604
605    /// Error details (on failure)
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub error: Option<ApiError>,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
611#[serde(rename_all = "lowercase")]
612pub enum ItemStatus {
613    Success,
614    Failed,
615    Skipped,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
619#[serde(rename_all = "snake_case")]
620pub enum BatchStatus {
621    AllSuccess,
622    PartialSuccess,
623    AllFailed,
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    #[test]
631    fn test_api_response_success() {
632        let response = ApiResponse::success("test data");
633        assert_eq!(response.status, ResponseStatus::Success);
634        assert!(response.data.is_some());
635        assert!(response.error.is_none());
636    }
637
638    #[test]
639    fn test_api_response_error() {
640        let error = ApiError::not_found("Resource not found");
641        let response: ApiResponse<String> = ApiResponse::error(error);
642        assert_eq!(response.status, ResponseStatus::Error);
643        assert!(response.data.is_none());
644        assert!(response.error.is_some());
645    }
646
647    #[test]
648    fn test_pagination_metadata() {
649        let pagination = PaginationMetadata::new(2, 50, 250);
650        assert_eq!(pagination.page, 2);
651        assert_eq!(pagination.total_pages, 5);
652        assert!(pagination.has_next);
653        assert!(pagination.has_previous);
654    }
655
656    #[test]
657    fn test_sse_message_format() {
658        let msg = SseMessage::event("update".to_string(), "{\"status\":\"ok\"}".to_string())
659            .with_id("123".to_string());
660
661        let formatted = msg.to_string();
662        assert!(formatted.contains("event: update"));
663        assert!(formatted.contains("id: 123"));
664        assert!(formatted.contains("data: {\"status\":\"ok\"}"));
665    }
666}