mkt_core/models/
creative.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct CreativeId(pub String);
10
11impl fmt::Display for CreativeId {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 write!(f, "{}", self.0)
14 }
15}
16
17impl<T: Into<String>> From<T> for CreativeId {
18 fn from(s: T) -> Self {
19 Self(s.into())
20 }
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Creative {
26 pub id: CreativeId,
28 pub provider: String,
30 pub name: String,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub body: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 pub image_url: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub video_url: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub link_url: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub call_to_action: Option<String>,
47 pub created_at: DateTime<Utc>,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub raw: Option<serde_json::Value>,
52}
53
54#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct CreateCreativeInput {
57 pub name: String,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub body: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub image_url: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub video_url: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub link_url: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub call_to_action: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub extra: Option<serde_json::Value>,
77}
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub struct CreateDarkPostInput {
82 pub page_id: String,
84 pub message: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub link: Option<String>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub image_url: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub call_to_action: Option<String>,
95}
96
97impl crate::output::Formattable for Creative {
98 fn headers() -> Vec<String> {
99 vec![
100 "ID".into(),
101 "Name".into(),
102 "Body".into(),
103 "Link URL".into(),
104 "Provider".into(),
105 ]
106 }
107
108 fn row(&self) -> Vec<String> {
109 vec![
110 self.id.to_string(),
111 self.name.clone(),
112 self.body
113 .as_deref()
114 .unwrap_or("-")
115 .chars()
116 .take(40)
117 .collect(),
118 self.link_url.clone().unwrap_or_else(|| "-".to_string()),
119 self.provider.clone(),
120 ]
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn creative_id_display() {
130 let id = CreativeId("creative_123".into());
131 assert_eq!(id.to_string(), "creative_123");
132 }
133
134 #[test]
135 fn creative_id_from_str() {
136 let id = CreativeId::from("creative_456");
137 assert_eq!(id.0, "creative_456");
138 }
139
140 #[test]
141 #[allow(clippy::expect_used)]
142 fn creative_id_serde_roundtrip() {
143 let id = CreativeId("creative_789".into());
144 let json = serde_json::to_string(&id).expect("serialize CreativeId");
145 let back: CreativeId = serde_json::from_str(&json).expect("deserialize CreativeId");
146 assert_eq!(id, back);
147 }
148
149 #[test]
150 fn create_creative_input_default() {
151 let input = CreateCreativeInput::default();
152 assert!(input.name.is_empty());
153 assert!(input.body.is_none());
154 assert!(input.image_url.is_none());
155 assert!(input.video_url.is_none());
156 assert!(input.link_url.is_none());
157 assert!(input.call_to_action.is_none());
158 assert!(input.extra.is_none());
159 }
160
161 #[test]
162 #[allow(clippy::expect_used)]
163 fn creative_optional_fields_skip_serializing_if_none() {
164 let now = chrono::Utc::now();
165 let creative = Creative {
166 id: CreativeId("c_1".into()),
167 provider: "meta".into(),
168 name: "Test Creative".into(),
169 body: None,
170 image_url: None,
171 video_url: None,
172 link_url: None,
173 call_to_action: None,
174 created_at: now,
175 raw: None,
176 };
177 let json = serde_json::to_string(&creative).expect("serialize Creative");
178 assert!(!json.contains("body"));
179 assert!(!json.contains("image_url"));
180 assert!(!json.contains("video_url"));
181 assert!(!json.contains("link_url"));
182 assert!(!json.contains("call_to_action"));
183 assert!(!json.contains("raw"));
184 }
185
186 #[test]
187 #[allow(clippy::expect_used)]
188 fn create_dark_post_input_serde_roundtrip() {
189 let input = CreateDarkPostInput {
190 page_id: "page_1".into(),
191 message: "Hello world".into(),
192 link: Some("https://example.com".into()),
193 image_url: None,
194 call_to_action: Some("LEARN_MORE".into()),
195 };
196 let json = serde_json::to_string(&input).expect("serialize CreateDarkPostInput");
197 let back: CreateDarkPostInput =
198 serde_json::from_str(&json).expect("deserialize CreateDarkPostInput");
199 assert_eq!(back.page_id, "page_1");
200 assert_eq!(back.message, "Hello world");
201 assert_eq!(back.link.as_deref(), Some("https://example.com"));
202 assert!(back.image_url.is_none());
203 assert_eq!(back.call_to_action.as_deref(), Some("LEARN_MORE"));
204 }
205
206 #[test]
207 #[allow(clippy::expect_used)]
208 fn create_dark_post_input_skips_none_fields() {
209 let input = CreateDarkPostInput {
210 page_id: "page_1".into(),
211 message: "Hello".into(),
212 link: None,
213 image_url: None,
214 call_to_action: None,
215 };
216 let json = serde_json::to_string(&input).expect("serialize");
217 assert!(!json.contains("link"));
218 assert!(!json.contains("image_url"));
219 assert!(!json.contains("call_to_action"));
220 }
221}