use std::fmt;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AudienceId(pub String);
impl fmt::Display for AudienceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<T: Into<String>> From<T> for AudienceId {
fn from(s: T) -> Self {
Self(s.into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Audience {
pub id: AudienceId,
pub provider: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
pub audience_type: AudienceType,
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AudienceType {
#[default]
Custom,
Lookalike,
SavedAudience,
Other(String),
}
impl fmt::Display for AudienceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Custom => write!(f, "custom"),
Self::Lookalike => write!(f, "lookalike"),
Self::SavedAudience => write!(f, "saved_audience"),
Self::Other(s) => write!(f, "{s}"),
}
}
}
impl<'de> Deserialize<'de> for AudienceType {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(match s.as_str() {
"custom" => Self::Custom,
"lookalike" => Self::Lookalike,
"saved_audience" => Self::SavedAudience,
_ => Self::Other(s),
})
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CreateAudienceInput {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub audience_type: AudienceType,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AudienceUser {
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AudienceUpdateResult {
pub audience_id: AudienceId,
pub num_received: u64,
pub num_invalid: u64,
}
impl crate::output::Formattable for Audience {
fn headers() -> Vec<String> {
vec![
"ID".into(),
"Name".into(),
"Type".into(),
"Size".into(),
"Provider".into(),
]
}
fn row(&self) -> Vec<String> {
vec![
self.id.to_string(),
self.name.clone(),
self.audience_type.to_string(),
self.size.map_or_else(|| "-".to_string(), |s| s.to_string()),
self.provider.clone(),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[test]
fn audience_id_display() {
let id = AudienceId("aud_123".into());
assert_eq!(id.to_string(), "aud_123");
}
#[test]
fn audience_id_from_str() {
let id = AudienceId::from("aud_456");
assert_eq!(id.0, "aud_456");
}
#[test]
#[allow(clippy::expect_used)]
fn audience_id_serde_roundtrip() {
let id = AudienceId("aud_789".into());
let json = serde_json::to_string(&id).expect("serialize AudienceId");
let back: AudienceId = serde_json::from_str(&json).expect("deserialize AudienceId");
assert_eq!(id, back);
}
#[test_case(AudienceType::Custom, "custom" ; "custom")]
#[test_case(AudienceType::Lookalike, "lookalike" ; "lookalike")]
#[test_case(AudienceType::SavedAudience, "saved_audience" ; "saved_audience")]
#[allow(clippy::needless_pass_by_value)]
fn audience_type_display(audience_type: AudienceType, expected: &str) {
assert_eq!(audience_type.to_string(), expected);
}
#[test]
fn audience_type_other_display() {
let t = AudienceType::Other("combined".into());
assert_eq!(t.to_string(), "combined");
}
#[test]
#[allow(clippy::expect_used)]
fn audience_type_serde_roundtrip() {
let json = serde_json::to_string(&AudienceType::Lookalike).expect("serialize");
assert_eq!(json, r#""lookalike""#);
let back: AudienceType = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, AudienceType::Lookalike);
}
#[test]
#[allow(clippy::expect_used)]
fn unknown_audience_type_deserializes_as_other() {
let t: AudienceType =
serde_json::from_str(r#""website_retargeting""#).expect("deserialize");
assert_eq!(t, AudienceType::Other("website_retargeting".into()));
}
#[test]
fn create_audience_input_default() {
let input = CreateAudienceInput::default();
assert!(input.name.is_empty());
assert!(input.description.is_none());
assert_eq!(input.audience_type, AudienceType::Custom);
assert!(input.extra.is_none());
}
#[test]
fn audience_user_default_all_none() {
let user = AudienceUser::default();
assert!(user.email.is_none());
assert!(user.phone.is_none());
assert!(user.external_id.is_none());
}
#[test]
#[allow(clippy::expect_used)]
fn audience_update_result_serde_roundtrip() {
let result = AudienceUpdateResult {
audience_id: AudienceId("aud_1".into()),
num_received: 100,
num_invalid: 5,
};
let json = serde_json::to_string(&result).expect("serialize AudienceUpdateResult");
let back: AudienceUpdateResult =
serde_json::from_str(&json).expect("deserialize AudienceUpdateResult");
assert_eq!(back.audience_id, result.audience_id);
assert_eq!(back.num_received, 100);
assert_eq!(back.num_invalid, 5);
}
#[test]
#[allow(clippy::expect_used)]
fn audience_optional_fields_skip_serializing_if_none() {
let now = chrono::Utc::now();
let audience = Audience {
id: AudienceId("aud_1".into()),
provider: "meta".into(),
name: "Test Audience".into(),
description: None,
size: None,
audience_type: AudienceType::Custom,
created_at: now,
raw: None,
};
let json = serde_json::to_string(&audience).expect("serialize Audience");
assert!(!json.contains("description"));
assert!(!json.contains("size"));
assert!(!json.contains("raw"));
}
}