1use std::fmt;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use super::common::Budget;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct CampaignId(pub String);
13
14impl fmt::Display for CampaignId {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 write!(f, "{}", self.0)
17 }
18}
19
20impl<T: Into<String>> From<T> for CampaignId {
21 fn from(s: T) -> Self {
22 Self(s.into())
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Campaign {
29 pub id: CampaignId,
31 pub provider: String,
33 pub name: String,
35 pub status: CampaignStatus,
37 pub objective: String,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub budget: Option<Budget>,
42 pub created_at: DateTime<Utc>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub updated_at: Option<DateTime<Utc>>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub raw: Option<serde_json::Value>,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54#[serde(rename_all = "snake_case")]
55pub enum CampaignStatus {
56 Active,
58 Paused,
60 Archived,
62 Draft,
64 Deleted,
66 Other(String),
68}
69
70impl fmt::Display for CampaignStatus {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 Self::Active => write!(f, "active"),
74 Self::Paused => write!(f, "paused"),
75 Self::Archived => write!(f, "archived"),
76 Self::Draft => write!(f, "draft"),
77 Self::Deleted => write!(f, "deleted"),
78 Self::Other(s) => write!(f, "{s}"),
79 }
80 }
81}
82
83impl<'de> Deserialize<'de> for CampaignStatus {
84 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
85 let s = String::deserialize(deserializer)?;
86 Ok(match s.as_str() {
87 "active" => Self::Active,
88 "paused" => Self::Paused,
89 "archived" => Self::Archived,
90 "draft" => Self::Draft,
91 "deleted" => Self::Deleted,
92 _ => Self::Other(s),
93 })
94 }
95}
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct CreateCampaignInput {
100 pub name: String,
102 pub objective: String,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub status: Option<CampaignStatus>,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub budget: Option<Budget>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub extra: Option<serde_json::Value>,
113}
114
115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
117pub struct UpdateCampaignInput {
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub name: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub status: Option<CampaignStatus>,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub budget: Option<Budget>,
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub extra: Option<serde_json::Value>,
130}
131
132#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134pub struct CampaignFilters {
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub status: Option<CampaignStatus>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub name_contains: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub limit: Option<u32>,
144 #[serde(skip_serializing_if = "Option::is_none")]
146 pub cursor: Option<String>,
147}
148
149impl crate::output::Formattable for Campaign {
150 fn headers() -> Vec<String> {
151 vec![
152 "ID".into(),
153 "Name".into(),
154 "Status".into(),
155 "Objective".into(),
156 "Provider".into(),
157 ]
158 }
159
160 fn row(&self) -> Vec<String> {
161 vec![
162 self.id.to_string(),
163 self.name.clone(),
164 self.status.to_string(),
165 self.objective.clone(),
166 self.provider.clone(),
167 ]
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use test_case::test_case;
175
176 #[test]
177 fn campaign_id_display() {
178 let id = CampaignId("camp_123".into());
179 assert_eq!(id.to_string(), "camp_123");
180 }
181
182 #[test]
183 fn campaign_id_from_str() {
184 let id = CampaignId::from("camp_456");
185 assert_eq!(id.0, "camp_456");
186 }
187
188 #[test_case(CampaignStatus::Active, "active" ; "active")]
189 #[test_case(CampaignStatus::Paused, "paused" ; "paused")]
190 #[test_case(CampaignStatus::Archived, "archived" ; "archived")]
191 #[test_case(CampaignStatus::Draft, "draft" ; "draft")]
192 #[test_case(CampaignStatus::Deleted, "deleted" ; "deleted")]
193 #[allow(clippy::needless_pass_by_value)]
194 fn campaign_status_display(status: CampaignStatus, expected: &str) {
195 assert_eq!(status.to_string(), expected);
196 }
197
198 #[test]
199 #[allow(clippy::expect_used)]
200 fn campaign_status_serde_roundtrip() {
201 let json = serde_json::to_string(&CampaignStatus::Active).expect("serialize");
202 assert_eq!(json, r#""active""#);
203 let back: CampaignStatus = serde_json::from_str(&json).expect("deserialize");
204 assert_eq!(back, CampaignStatus::Active);
205 }
206
207 #[test]
208 #[allow(clippy::expect_used)]
209 fn unknown_status_deserializes_as_other() {
210 let status: CampaignStatus =
211 serde_json::from_str(r#""something_new""#).expect("deserialize");
212 assert_eq!(status, CampaignStatus::Other("something_new".into()));
213 }
214
215 #[test]
216 fn campaign_filters_default_is_empty() {
217 let filters = CampaignFilters::default();
218 assert!(filters.status.is_none());
219 assert!(filters.name_contains.is_none());
220 assert!(filters.limit.is_none());
221 assert!(filters.cursor.is_none());
222 }
223
224 #[test]
225 fn create_campaign_input_default() {
226 let input = CreateCampaignInput::default();
227 assert!(input.name.is_empty());
228 assert!(input.objective.is_empty());
229 assert!(input.status.is_none());
230 }
231}