1use std::collections::HashMap;
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8use super::common::DateRange;
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct InsightsQuery {
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub date_range: Option<DateRange>,
16 pub metrics: Vec<String>,
18 pub breakdowns: Vec<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub level: Option<InsightsLevel>,
23 pub entity_ids: Vec<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub limit: Option<u32>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum InsightsLevel {
34 Account,
36 Campaign,
38 AdSet,
40 Ad,
42}
43
44impl fmt::Display for InsightsLevel {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::Account => write!(f, "account"),
48 Self::Campaign => write!(f, "campaign"),
49 Self::AdSet => write!(f, "ad_set"),
50 Self::Ad => write!(f, "ad"),
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct InsightsReport {
58 pub provider: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub date_range: Option<DateRange>,
63 pub rows: Vec<InsightsRow>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub raw: Option<serde_json::Value>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct InsightsRow {
73 pub dimensions: HashMap<String, String>,
75 pub metrics: HashMap<String, MetricValue>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct MetricValue {
82 pub value: f64,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub formatted: Option<String>,
87}
88
89impl crate::output::Formattable for InsightsRow {
90 fn headers() -> Vec<String> {
91 vec!["Dimensions".into(), "Metrics".into()]
92 }
93
94 fn row(&self) -> Vec<String> {
95 let dims: Vec<String> = self
96 .dimensions
97 .iter()
98 .map(|(k, v)| format!("{k}={v}"))
99 .collect();
100 let mets: Vec<String> = self
101 .metrics
102 .iter()
103 .map(|(k, v)| {
104 v.formatted
105 .as_deref()
106 .map_or_else(|| format!("{k}={}", v.value), |f| format!("{k}={f}"))
107 })
108 .collect();
109 vec![dims.join(", "), mets.join(", ")]
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use test_case::test_case;
117
118 #[test_case(InsightsLevel::Account, "account" ; "account")]
119 #[test_case(InsightsLevel::Campaign, "campaign" ; "campaign")]
120 #[test_case(InsightsLevel::AdSet, "ad_set" ; "adset")]
121 #[test_case(InsightsLevel::Ad, "ad" ; "ad")]
122 #[allow(clippy::needless_pass_by_value)]
123 fn insights_level_display(level: InsightsLevel, expected: &str) {
124 assert_eq!(level.to_string(), expected);
125 }
126
127 #[test]
128 #[allow(clippy::expect_used)]
129 fn insights_level_serde_roundtrip() {
130 let json = serde_json::to_string(&InsightsLevel::Campaign).expect("serialize");
131 assert_eq!(json, r#""campaign""#);
132 let back: InsightsLevel = serde_json::from_str(&json).expect("deserialize");
133 assert_eq!(back, InsightsLevel::Campaign);
134 }
135
136 #[test]
137 #[allow(clippy::expect_used)]
138 fn insights_level_adset_serde_roundtrip() {
139 let json = serde_json::to_string(&InsightsLevel::AdSet).expect("serialize");
140 assert_eq!(json, r#""ad_set""#);
141 let back: InsightsLevel = serde_json::from_str(&json).expect("deserialize");
142 assert_eq!(back, InsightsLevel::AdSet);
143 }
144
145 #[test]
146 fn insights_query_default() {
147 let query = InsightsQuery::default();
148 assert!(query.date_range.is_none());
149 assert!(query.metrics.is_empty());
150 assert!(query.breakdowns.is_empty());
151 assert!(query.level.is_none());
152 assert!(query.entity_ids.is_empty());
153 assert!(query.limit.is_none());
154 }
155
156 #[test]
157 #[allow(clippy::expect_used)]
158 fn insights_query_serde_roundtrip() {
159 let query = InsightsQuery {
160 date_range: None,
161 metrics: vec!["impressions".into(), "clicks".into()],
162 breakdowns: vec!["age".into()],
163 level: Some(InsightsLevel::Campaign),
164 entity_ids: vec!["camp_1".into()],
165 limit: Some(100),
166 };
167 let json = serde_json::to_string(&query).expect("serialize InsightsQuery");
168 let back: InsightsQuery = serde_json::from_str(&json).expect("deserialize InsightsQuery");
169 assert_eq!(back.metrics, vec!["impressions", "clicks"]);
170 assert_eq!(back.breakdowns, vec!["age"]);
171 assert_eq!(back.level, Some(InsightsLevel::Campaign));
172 assert_eq!(back.limit, Some(100));
173 }
174
175 #[test]
176 #[allow(clippy::expect_used)]
177 fn metric_value_serde_roundtrip() {
178 let mv = MetricValue {
179 value: 1234.56,
180 formatted: Some("$1,234.56".into()),
181 };
182 let json = serde_json::to_string(&mv).expect("serialize MetricValue");
183 let back: MetricValue = serde_json::from_str(&json).expect("deserialize MetricValue");
184 assert!((back.value - 1234.56).abs() < f64::EPSILON);
185 assert_eq!(back.formatted.as_deref(), Some("$1,234.56"));
186 }
187
188 #[test]
189 #[allow(clippy::expect_used)]
190 fn metric_value_skips_none_formatted() {
191 let mv = MetricValue {
192 value: 42.0,
193 formatted: None,
194 };
195 let json = serde_json::to_string(&mv).expect("serialize MetricValue");
196 assert!(!json.contains("formatted"));
197 }
198
199 #[test]
200 #[allow(clippy::expect_used)]
201 fn insights_row_serde_roundtrip() {
202 let mut dimensions = HashMap::new();
203 dimensions.insert("age".into(), "25-34".into());
204 let mut metrics = HashMap::new();
205 metrics.insert(
206 "clicks".into(),
207 MetricValue {
208 value: 42.0,
209 formatted: None,
210 },
211 );
212 let row = InsightsRow {
213 dimensions,
214 metrics,
215 };
216 let json = serde_json::to_string(&row).expect("serialize InsightsRow");
217 let back: InsightsRow = serde_json::from_str(&json).expect("deserialize InsightsRow");
218 assert_eq!(
219 back.dimensions.get("age").map(String::as_str),
220 Some("25-34")
221 );
222 assert!((back.metrics["clicks"].value - 42.0).abs() < f64::EPSILON);
223 }
224
225 #[test]
226 #[allow(clippy::expect_used)]
227 fn insights_report_skips_none_fields() {
228 let report = InsightsReport {
229 provider: "meta".into(),
230 date_range: None,
231 rows: vec![],
232 raw: None,
233 };
234 let json = serde_json::to_string(&report).expect("serialize InsightsReport");
235 assert!(!json.contains("date_range"));
236 assert!(!json.contains("raw"));
237 }
238}