1use std::fmt;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct AudienceId(pub String);
11
12impl fmt::Display for AudienceId {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 write!(f, "{}", self.0)
15 }
16}
17
18impl<T: Into<String>> From<T> for AudienceId {
19 fn from(s: T) -> Self {
20 Self(s.into())
21 }
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Audience {
27 pub id: AudienceId,
29 pub provider: String,
31 pub name: String,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub description: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub size: Option<u64>,
39 pub audience_type: AudienceType,
41 pub created_at: DateTime<Utc>,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub raw: Option<serde_json::Value>,
46}
47
48#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
50#[serde(rename_all = "snake_case")]
51pub enum AudienceType {
52 #[default]
54 Custom,
55 Lookalike,
57 SavedAudience,
59 Other(String),
61}
62
63impl fmt::Display for AudienceType {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 Self::Custom => write!(f, "custom"),
67 Self::Lookalike => write!(f, "lookalike"),
68 Self::SavedAudience => write!(f, "saved_audience"),
69 Self::Other(s) => write!(f, "{s}"),
70 }
71 }
72}
73
74impl<'de> Deserialize<'de> for AudienceType {
75 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
76 let s = String::deserialize(deserializer)?;
77 Ok(match s.as_str() {
78 "custom" => Self::Custom,
79 "lookalike" => Self::Lookalike,
80 "saved_audience" => Self::SavedAudience,
81 _ => Self::Other(s),
82 })
83 }
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct CreateAudienceInput {
89 pub name: String,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub description: Option<String>,
94 pub audience_type: AudienceType,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub extra: Option<serde_json::Value>,
99}
100
101#[derive(Debug, Clone, Default, Serialize, Deserialize)]
103pub struct AudienceUser {
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub email: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub phone: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub external_id: Option<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct AudienceUpdateResult {
118 pub audience_id: AudienceId,
120 pub num_received: u64,
122 pub num_invalid: u64,
124}
125
126impl crate::output::Formattable for Audience {
127 fn headers() -> Vec<String> {
128 vec![
129 "ID".into(),
130 "Name".into(),
131 "Type".into(),
132 "Size".into(),
133 "Provider".into(),
134 ]
135 }
136
137 fn row(&self) -> Vec<String> {
138 vec![
139 self.id.to_string(),
140 self.name.clone(),
141 self.audience_type.to_string(),
142 self.size.map_or_else(|| "-".to_string(), |s| s.to_string()),
143 self.provider.clone(),
144 ]
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use test_case::test_case;
152
153 #[test]
154 fn audience_id_display() {
155 let id = AudienceId("aud_123".into());
156 assert_eq!(id.to_string(), "aud_123");
157 }
158
159 #[test]
160 fn audience_id_from_str() {
161 let id = AudienceId::from("aud_456");
162 assert_eq!(id.0, "aud_456");
163 }
164
165 #[test]
166 #[allow(clippy::expect_used)]
167 fn audience_id_serde_roundtrip() {
168 let id = AudienceId("aud_789".into());
169 let json = serde_json::to_string(&id).expect("serialize AudienceId");
170 let back: AudienceId = serde_json::from_str(&json).expect("deserialize AudienceId");
171 assert_eq!(id, back);
172 }
173
174 #[test_case(AudienceType::Custom, "custom" ; "custom")]
175 #[test_case(AudienceType::Lookalike, "lookalike" ; "lookalike")]
176 #[test_case(AudienceType::SavedAudience, "saved_audience" ; "saved_audience")]
177 #[allow(clippy::needless_pass_by_value)]
178 fn audience_type_display(audience_type: AudienceType, expected: &str) {
179 assert_eq!(audience_type.to_string(), expected);
180 }
181
182 #[test]
183 fn audience_type_other_display() {
184 let t = AudienceType::Other("combined".into());
185 assert_eq!(t.to_string(), "combined");
186 }
187
188 #[test]
189 #[allow(clippy::expect_used)]
190 fn audience_type_serde_roundtrip() {
191 let json = serde_json::to_string(&AudienceType::Lookalike).expect("serialize");
192 assert_eq!(json, r#""lookalike""#);
193 let back: AudienceType = serde_json::from_str(&json).expect("deserialize");
194 assert_eq!(back, AudienceType::Lookalike);
195 }
196
197 #[test]
198 #[allow(clippy::expect_used)]
199 fn unknown_audience_type_deserializes_as_other() {
200 let t: AudienceType =
201 serde_json::from_str(r#""website_retargeting""#).expect("deserialize");
202 assert_eq!(t, AudienceType::Other("website_retargeting".into()));
203 }
204
205 #[test]
206 fn create_audience_input_default() {
207 let input = CreateAudienceInput::default();
208 assert!(input.name.is_empty());
209 assert!(input.description.is_none());
210 assert_eq!(input.audience_type, AudienceType::Custom);
211 assert!(input.extra.is_none());
212 }
213
214 #[test]
215 fn audience_user_default_all_none() {
216 let user = AudienceUser::default();
217 assert!(user.email.is_none());
218 assert!(user.phone.is_none());
219 assert!(user.external_id.is_none());
220 }
221
222 #[test]
223 #[allow(clippy::expect_used)]
224 fn audience_update_result_serde_roundtrip() {
225 let result = AudienceUpdateResult {
226 audience_id: AudienceId("aud_1".into()),
227 num_received: 100,
228 num_invalid: 5,
229 };
230 let json = serde_json::to_string(&result).expect("serialize AudienceUpdateResult");
231 let back: AudienceUpdateResult =
232 serde_json::from_str(&json).expect("deserialize AudienceUpdateResult");
233 assert_eq!(back.audience_id, result.audience_id);
234 assert_eq!(back.num_received, 100);
235 assert_eq!(back.num_invalid, 5);
236 }
237
238 #[test]
239 #[allow(clippy::expect_used)]
240 fn audience_optional_fields_skip_serializing_if_none() {
241 let now = chrono::Utc::now();
242 let audience = Audience {
243 id: AudienceId("aud_1".into()),
244 provider: "meta".into(),
245 name: "Test Audience".into(),
246 description: None,
247 size: None,
248 audience_type: AudienceType::Custom,
249 created_at: now,
250 raw: None,
251 };
252 let json = serde_json::to_string(&audience).expect("serialize Audience");
253 assert!(!json.contains("description"));
254 assert!(!json.contains("size"));
255 assert!(!json.contains("raw"));
256 }
257}