Skip to main content

mkt_core/models/
common.rs

1//! Common types shared across all domain models.
2
3use std::fmt;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8/// A paginated response wrapper.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Paginated<T> {
11    /// The items in this page.
12    pub data: Vec<T>,
13    /// Cursor for the next page, if any.
14    pub next_cursor: Option<String>,
15    /// Total number of items across all pages, if known.
16    pub total: Option<u64>,
17}
18
19impl<T> Default for Paginated<T> {
20    fn default() -> Self {
21        Self {
22            data: Vec::new(),
23            next_cursor: None,
24            total: None,
25        }
26    }
27}
28
29/// A monetary budget.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct Budget {
32    /// Amount in the smallest currency unit (e.g. cents).
33    pub amount: f64,
34    /// ISO 4217 currency code (e.g. "USD").
35    pub currency: String,
36    /// Whether this is a daily or lifetime budget.
37    pub kind: BudgetKind,
38}
39
40/// The type of budget allocation.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum BudgetKind {
44    /// Budget resets daily.
45    Daily,
46    /// Budget is for the entire campaign lifetime.
47    Lifetime,
48}
49
50impl fmt::Display for BudgetKind {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            Self::Daily => write!(f, "daily"),
54            Self::Lifetime => write!(f, "lifetime"),
55        }
56    }
57}
58
59/// A date range for filtering or reporting.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct DateRange {
62    /// Start of the range (inclusive).
63    pub start: DateTime<Utc>,
64    /// End of the range (inclusive).
65    pub end: DateTime<Utc>,
66}
67
68/// HTTP methods for the raw escape hatch.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "UPPERCASE")]
71pub enum HttpMethod {
72    /// HTTP GET.
73    Get,
74    /// HTTP POST.
75    Post,
76    /// HTTP DELETE.
77    Delete,
78}
79
80impl fmt::Display for HttpMethod {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            Self::Get => write!(f, "GET"),
84            Self::Post => write!(f, "POST"),
85            Self::Delete => write!(f, "DELETE"),
86        }
87    }
88}
89
90/// Health check result from a provider.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ProviderHealth {
93    /// Provider name.
94    pub provider: String,
95    /// Whether the provider is reachable and authenticated.
96    pub healthy: bool,
97    /// Round-trip latency in milliseconds.
98    pub latency_ms: u64,
99    /// Optional status message.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub message: Option<String>,
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn paginated_default_is_empty() {
110        let page: Paginated<String> = Paginated::default();
111        assert!(page.data.is_empty());
112        assert!(page.next_cursor.is_none());
113        assert!(page.total.is_none());
114    }
115
116    #[test]
117    fn budget_kind_display() {
118        assert_eq!(BudgetKind::Daily.to_string(), "daily");
119        assert_eq!(BudgetKind::Lifetime.to_string(), "lifetime");
120    }
121
122    #[test]
123    #[allow(clippy::expect_used)]
124    fn budget_kind_serde_roundtrip() {
125        let json = serde_json::to_string(&BudgetKind::Daily).expect("serialize");
126        assert_eq!(json, r#""daily""#);
127        let back: BudgetKind = serde_json::from_str(&json).expect("deserialize");
128        assert_eq!(back, BudgetKind::Daily);
129    }
130
131    #[test]
132    fn http_method_display() {
133        assert_eq!(HttpMethod::Get.to_string(), "GET");
134        assert_eq!(HttpMethod::Post.to_string(), "POST");
135        assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
136    }
137
138    #[test]
139    #[allow(clippy::expect_used)]
140    fn http_method_serde_roundtrip() {
141        let json = serde_json::to_string(&HttpMethod::Post).expect("serialize");
142        assert_eq!(json, r#""POST""#);
143        let back: HttpMethod = serde_json::from_str(&json).expect("deserialize");
144        assert_eq!(back, HttpMethod::Post);
145    }
146
147    #[test]
148    #[allow(clippy::expect_used)]
149    fn provider_health_serializes_without_none_message() {
150        let health = ProviderHealth {
151            provider: "meta".into(),
152            healthy: true,
153            latency_ms: 42,
154            message: None,
155        };
156        let json = serde_json::to_string(&health).expect("serialize");
157        assert!(!json.contains("message"));
158    }
159}