1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4pub const MIN_INSIGHT_TIMESTAMP: i64 = 1_712_991_600;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub enum PostInsightMetric {
14 #[serde(rename = "views")]
16 Views,
17 #[serde(rename = "likes")]
19 Likes,
20 #[serde(rename = "replies")]
22 Replies,
23 #[serde(rename = "reposts")]
25 Reposts,
26 #[serde(rename = "quotes")]
28 Quotes,
29 #[serde(rename = "shares")]
31 Shares,
32}
33
34impl PostInsightMetric {
35 pub fn all() -> &'static [Self] {
37 &[
38 Self::Views,
39 Self::Likes,
40 Self::Replies,
41 Self::Reposts,
42 Self::Quotes,
43 Self::Shares,
44 ]
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub enum AccountInsightMetric {
51 #[serde(rename = "views")]
53 Views,
54 #[serde(rename = "likes")]
56 Likes,
57 #[serde(rename = "replies")]
59 Replies,
60 #[serde(rename = "reposts")]
62 Reposts,
63 #[serde(rename = "quotes")]
65 Quotes,
66 #[serde(rename = "clicks")]
68 Clicks,
69 #[serde(rename = "followers_count")]
71 FollowersCount,
72 #[serde(rename = "follower_demographics")]
74 FollowerDemographics,
75}
76
77impl AccountInsightMetric {
79 pub fn all() -> &'static [Self] {
81 &[
82 Self::Views,
83 Self::Likes,
84 Self::Replies,
85 Self::Reposts,
86 Self::Quotes,
87 Self::Clicks,
88 Self::FollowersCount,
89 Self::FollowerDemographics,
90 ]
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub enum InsightPeriod {
97 #[serde(rename = "day")]
99 Day,
100 #[serde(rename = "lifetime")]
102 Lifetime,
103}
104
105impl InsightPeriod {
106 pub fn all() -> &'static [Self] {
108 &[Self::Day, Self::Lifetime]
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub enum FollowerDemographicsBreakdown {
115 #[serde(rename = "country")]
117 Country,
118 #[serde(rename = "city")]
120 City,
121 #[serde(rename = "age")]
123 Age,
124 #[serde(rename = "gender")]
126 Gender,
127}
128
129impl FollowerDemographicsBreakdown {
130 pub fn all() -> &'static [Self] {
132 &[Self::Country, Self::City, Self::Age, Self::Gender]
133 }
134}
135
136#[derive(Debug, Clone, Default, Serialize, Deserialize)]
142pub struct PostInsightsOptions {
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub metrics: Option<Vec<PostInsightMetric>>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub period: Option<InsightPeriod>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub since: Option<DateTime<Utc>>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub until: Option<DateTime<Utc>>,
155}
156
157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
159pub struct AccountInsightsOptions {
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub metrics: Option<Vec<AccountInsightMetric>>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub period: Option<InsightPeriod>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub since: Option<DateTime<Utc>>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub until: Option<DateTime<Utc>>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub breakdown: Option<FollowerDemographicsBreakdown>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Insight {
184 pub name: String,
186 pub period: String,
188 #[serde(default)]
190 pub values: Vec<Value>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub title: Option<String>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub description: Option<String>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub id: Option<String>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub total_value: Option<TotalValue>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub link_total_values: Option<Vec<LinkTotalValue>>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub demographic_total_value: Option<DemographicTotalValue>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct Value {
214 pub value: i64,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub end_time: Option<String>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct TotalValue {
224 pub value: i64,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub link_url: Option<String>,
229}
230
231#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct LinkTotalValue {
234 pub value: i64,
236 pub link_url: String,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct DemographicResult {
243 #[serde(default)]
245 pub dimension_values: Vec<String>,
246 pub value: f64,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct DemographicBreakdown {
253 pub dimension_keys: Vec<String>,
255 pub results: Vec<DemographicResult>,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct DemographicTotalValue {
262 pub breakdowns: Vec<DemographicBreakdown>,
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_post_insight_metric_all() {
272 let all = PostInsightMetric::all();
273 assert_eq!(all.len(), 6);
274 assert_eq!(all[0], PostInsightMetric::Views);
275 assert_eq!(all[5], PostInsightMetric::Shares);
276 }
277
278 #[test]
279 fn test_account_insight_metric_all() {
280 let all = AccountInsightMetric::all();
281 assert_eq!(all.len(), 8);
282 assert_eq!(all[0], AccountInsightMetric::Views);
283 assert_eq!(all[7], AccountInsightMetric::FollowerDemographics);
284 }
285
286 #[test]
287 fn test_insight_period_all() {
288 let all = InsightPeriod::all();
289 assert_eq!(all.len(), 2);
290 assert_eq!(all[0], InsightPeriod::Day);
291 assert_eq!(all[1], InsightPeriod::Lifetime);
292 }
293
294 #[test]
295 fn test_follower_demographics_breakdown_all() {
296 let all = FollowerDemographicsBreakdown::all();
297 assert_eq!(all.len(), 4);
298 assert_eq!(all[0], FollowerDemographicsBreakdown::Country);
299 assert_eq!(all[3], FollowerDemographicsBreakdown::Gender);
300 }
301
302 #[test]
303 fn test_post_insight_metric_serde() {
304 let metric = PostInsightMetric::Views;
305 let json = serde_json::to_string(&metric).unwrap();
306 assert_eq!(json, r#""views""#);
307 let back: PostInsightMetric = serde_json::from_str(&json).unwrap();
308 assert_eq!(back, PostInsightMetric::Views);
309 }
310
311 #[test]
312 fn test_insight_period_serde() {
313 let period = InsightPeriod::Day;
314 let json = serde_json::to_string(&period).unwrap();
315 assert_eq!(json, r#""day""#);
316 }
317
318 #[test]
319 fn test_post_insights_options_default() {
320 let opts = PostInsightsOptions::default();
321 assert!(opts.metrics.is_none());
322 assert!(opts.period.is_none());
323 assert!(opts.since.is_none());
324 assert!(opts.until.is_none());
325 }
326
327 #[test]
328 fn test_account_insights_options_default() {
329 let opts = AccountInsightsOptions::default();
330 assert!(opts.metrics.is_none());
331 assert!(opts.breakdown.is_none());
332 }
333
334 #[test]
335 fn test_total_value_with_link_url() {
336 let json = r#"{"value": 42, "link_url": "https://example.com"}"#;
337 let tv: TotalValue = serde_json::from_str(json).unwrap();
338 assert_eq!(tv.value, 42);
339 assert_eq!(tv.link_url.as_deref(), Some("https://example.com"));
340 }
341
342 #[test]
343 fn test_total_value_without_link_url() {
344 let json = r#"{"value": 42}"#;
345 let tv: TotalValue = serde_json::from_str(json).unwrap();
346 assert_eq!(tv.value, 42);
347 assert!(tv.link_url.is_none());
348 }
349
350 #[test]
351 fn test_link_total_value_deserialize() {
352 let json = r#"{"value": 11, "link_url": "https://example.com"}"#;
353 let ltv: LinkTotalValue = serde_json::from_str(json).unwrap();
354 assert_eq!(ltv.value, 11);
355 assert_eq!(ltv.link_url, "https://example.com");
356 }
357
358 #[test]
359 fn test_insight_with_link_total_values() {
360 let json = r#"{
361 "name": "clicks",
362 "period": "lifetime",
363 "link_total_values": [
364 {"value": 11, "link_url": "https://example.com"},
365 {"value": 5, "link_url": "https://other.com"}
366 ]
367 }"#;
368 let insight: Insight = serde_json::from_str(json).unwrap();
369 assert_eq!(insight.name, "clicks");
370 let ltv = insight.link_total_values.unwrap();
371 assert_eq!(ltv.len(), 2);
372 assert_eq!(ltv[0].value, 11);
373 assert_eq!(ltv[1].link_url, "https://other.com");
374 }
375
376 #[test]
377 fn test_demographic_breakdown_deserialize() {
378 let json = r#"{
379 "name": "follower_demographics",
380 "period": "lifetime",
381 "demographic_total_value": {
382 "breakdowns": [{
383 "dimension_keys": ["country"],
384 "results": [
385 {"dimension_values": ["US"], "value": 100.0},
386 {"dimension_values": ["GB"], "value": 50.0}
387 ]
388 }]
389 }
390 }"#;
391 let insight: Insight = serde_json::from_str(json).unwrap();
392 assert_eq!(insight.name, "follower_demographics");
393 let demo = insight.demographic_total_value.unwrap();
394 assert_eq!(demo.breakdowns.len(), 1);
395 assert_eq!(demo.breakdowns[0].dimension_keys, vec!["country"]);
396 assert_eq!(demo.breakdowns[0].results.len(), 2);
397 assert_eq!(demo.breakdowns[0].results[0].dimension_values, vec!["US"]);
398 assert_eq!(demo.breakdowns[0].results[0].value, 100.0);
399 }
400}