apple_search_ads/objects/
reporting_request.rs

1// https://developer.apple.com/documentation/apple_search_ads/reportingrequest
2
3use chrono::{Datelike as _, NaiveDate, Utc};
4use serde::{Deserialize, Serialize};
5use serde_aux_ext::field_attributes::deserialize_option_bool_from_anything;
6
7use crate::objects::selector::Selector;
8
9//
10#[derive(Deserialize, Serialize, Debug, Clone)]
11pub struct ReportingRequest {
12    #[serde(with = "reporting_request_date_format")]
13    #[serde(rename = "startTime")]
14    pub start_time: NaiveDate,
15
16    #[serde(with = "reporting_request_date_format")]
17    #[serde(rename = "endTime")]
18    pub end_time: NaiveDate,
19
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub granularity: Option<ReportingRequestGranularity>,
22
23    #[serde(rename = "groupBy", skip_serializing_if = "Option::is_none")]
24    pub group_by: Option<Vec<ReportingRequestGroupBy>>,
25
26    #[serde(
27        default,
28        deserialize_with = "deserialize_option_bool_from_anything",
29        rename = "returnGrandTotals",
30        skip_serializing_if = "Option::is_none"
31    )]
32    pub return_grand_totals: Option<bool>,
33
34    #[serde(
35        default,
36        deserialize_with = "deserialize_option_bool_from_anything",
37        rename = "returnRecordsWithNoMetrics",
38        skip_serializing_if = "Option::is_none"
39    )]
40    pub return_records_with_no_metrics: Option<bool>,
41
42    #[serde(
43        default,
44        deserialize_with = "deserialize_option_bool_from_anything",
45        rename = "returnRowTotals",
46        skip_serializing_if = "Option::is_none"
47    )]
48    pub return_row_totals: Option<bool>,
49
50    pub selector: Selector,
51
52    #[serde(rename = "timeZone", skip_serializing_if = "Option::is_none")]
53    pub time_zone: Option<ReportingRequestTimeZone>,
54}
55
56impl Default for ReportingRequest {
57    fn default() -> Self {
58        let now = Utc::now();
59        let now = NaiveDate::from_ymd_opt(now.year(), now.month(), now.day()).expect("");
60        Self::new(now - chrono::Duration::days(3), now, Selector::default())
61    }
62}
63
64impl ReportingRequest {
65    pub fn new(start_time: NaiveDate, end_time: NaiveDate, selector: Selector) -> Self {
66        Self {
67            start_time,
68            end_time,
69            granularity: None,
70            group_by: None,
71            return_grand_totals: None,
72            return_records_with_no_metrics: None,
73            return_row_totals: None,
74            selector,
75            time_zone: None,
76        }
77    }
78
79    pub fn set_granularity(
80        &mut self,
81        val: impl Into<Option<ReportingRequestGranularity>>,
82    ) -> &mut Self {
83        self.granularity = val.into();
84
85        // https://developer.apple.com/documentation/apple_search_ads/row
86        // Note: if granularity is specified in the payload, then returnRowTotals and returnGrandTotals must be false
87        self.return_row_totals = Some(false);
88        self.return_grand_totals = Some(false);
89
90        self
91    }
92
93    pub fn set_group_by(
94        &mut self,
95        val: impl Into<Option<Vec<ReportingRequestGroupBy>>>,
96    ) -> &mut Self {
97        self.group_by = val.into();
98        self
99    }
100
101    pub fn set_return_grand_totals(&mut self, val: impl Into<Option<bool>>) -> &mut Self {
102        if self.granularity.is_none() {
103            self.return_grand_totals = val.into();
104        }
105        self
106    }
107
108    pub fn set_return_records_with_no_metrics(
109        &mut self,
110        val: impl Into<Option<bool>>,
111    ) -> &mut Self {
112        self.return_records_with_no_metrics = val.into();
113        self
114    }
115
116    pub fn set_return_row_totals(&mut self, val: impl Into<Option<bool>>) -> &mut Self {
117        if self.granularity.is_none() {
118            self.return_row_totals = val.into();
119        }
120        self
121    }
122
123    pub fn set_time_zone(&mut self, val: impl Into<Option<ReportingRequestTimeZone>>) -> &mut Self {
124        self.time_zone = val.into();
125        self
126    }
127}
128
129pub mod reporting_request_date_format {
130    use chrono::NaiveDate;
131    use serde::{self, Deserialize, Deserializer, Serializer};
132
133    const FORMAT: &str = "%Y-%m-%d";
134
135    pub fn serialize<S>(date: &NaiveDate, serializer: S) -> Result<S::Ok, S::Error>
136    where
137        S: Serializer,
138    {
139        let s = format!("{}", date.format(FORMAT));
140        serializer.serialize_str(&s)
141    }
142
143    pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
144    where
145        D: Deserializer<'de>,
146    {
147        let s = String::deserialize(deserializer)?;
148        NaiveDate::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)
149    }
150}
151
152#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
153pub enum ReportingRequestGroupBy {
154    #[serde(rename = "deviceClass")]
155    DeviceClass,
156    #[serde(rename = "ageRange")]
157    AgeRange,
158    #[serde(rename = "gender")]
159    Gender,
160    #[serde(rename = "countryCode")]
161    CountryCode,
162    #[serde(rename = "adminArea")]
163    AdminArea,
164    #[serde(rename = "locality")]
165    Locality,
166    #[serde(rename = "countryOrRegion")]
167    CountryOrRegion,
168}
169
170#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
171pub enum ReportingRequestTimeZone {
172    #[allow(clippy::upper_case_acronyms)]
173    UTC,
174    #[allow(clippy::upper_case_acronyms)]
175    ORTZ,
176}
177
178#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
179pub enum ReportingRequestGranularity {
180    #[allow(clippy::upper_case_acronyms)]
181    MONTHLY,
182    #[allow(clippy::upper_case_acronyms)]
183    WEEKLY,
184    #[allow(clippy::upper_case_acronyms)]
185    DAILY,
186    #[allow(clippy::upper_case_acronyms)]
187    HOURLY,
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    use std::error;
195
196    use serde_json::Value;
197
198    use crate::objects::{
199        condition::{Condition, ConditionOperator::*},
200        pagination::Pagination,
201        reporting_request::{
202            ReportingRequestGranularity::*, ReportingRequestGroupBy::*, ReportingRequestTimeZone::*,
203        },
204        sorting::{Sorting, SortingSortOrder::*},
205    };
206
207    #[test]
208    fn test_v4_ser_get_campaign_level_reports() -> Result<(), Box<dyn error::Error>> {
209        let mut pagination = Pagination::new();
210        pagination.set_limit(1000).set_offset(0);
211
212        let mut selector = Selector::new(vec![Sorting::new("countryOrRegion", ASCENDING)]);
213        selector
214            .set_conditions(vec![
215                Condition::new("countriesOrRegions", CONTAINS_ANY, vec!["US", "GB"]),
216                Condition::new("countryOrRegion", IN, vec!["US"]),
217            ])
218            .set_pagination(pagination);
219
220        let mut reporting_request = ReportingRequest::new(
221            "2021-04-08".parse().unwrap(),
222            "2021-04-09".parse().unwrap(),
223            selector,
224        );
225        reporting_request
226            .set_group_by(vec![CountryOrRegion])
227            .set_time_zone(UTC)
228            .set_return_records_with_no_metrics(true)
229            .set_return_row_totals(true)
230            .set_return_grand_totals(true);
231
232        let value1: Value = serde_json::to_value(reporting_request)?;
233
234        let json_content =
235            include_str!("../../tests/v4/request_body_json_files/get_campaign_level_reports_payload_example_1.json");
236        let value2: Value = serde_json::from_str(json_content)?;
237
238        assert_eq!(value1, value2);
239
240        Ok(())
241    }
242
243    #[test]
244    fn test_v4_ser_get_campaign_level_reports_with_granularity() -> Result<(), Box<dyn error::Error>>
245    {
246        let mut pagination = Pagination::new();
247        pagination.set_limit(1000).set_offset(0);
248
249        let mut selector = Selector::new(vec![Sorting::new("countryOrRegion", ASCENDING)]);
250        selector
251            .set_conditions(vec![
252                Condition::new("countriesOrRegions", CONTAINS_ANY, vec!["US", "GB"]),
253                Condition::new("countryOrRegion", IN, vec!["US"]),
254            ])
255            .set_pagination(pagination);
256
257        let mut reporting_request = ReportingRequest::new(
258            "2021-04-08".parse().unwrap(),
259            "2021-04-18".parse().unwrap(),
260            selector,
261        );
262        reporting_request
263            .set_group_by(vec![CountryOrRegion])
264            .set_time_zone(UTC)
265            .set_return_records_with_no_metrics(true)
266            .set_return_row_totals(false)
267            .set_granularity(DAILY)
268            .set_return_grand_totals(false);
269
270        let value1: Value = serde_json::to_value(reporting_request)?;
271
272        let json_content = include_str!(
273            "../../tests/v4/request_body_json_files/get_campaign_level_reports_payload_example_3.json",
274        );
275        let value2: Value = serde_json::from_str(json_content)?;
276
277        assert_eq!(value1, value2);
278
279        Ok(())
280    }
281
282    #[test]
283    fn test_v3_ser_get_campaign_level_reports() -> Result<(), Box<dyn error::Error>> {
284        let mut pagination = Pagination::new();
285        pagination.set_limit(1000).set_offset(0);
286
287        let mut selector = Selector::new(vec![Sorting::new("countryOrRegion", ASCENDING)]);
288        selector
289            .set_conditions(vec![
290                Condition::new("countriesOrRegions", CONTAINS_ANY, vec!["US", "GB"]),
291                Condition::new("countryOrRegion", IN, vec!["US"]),
292            ])
293            .set_pagination(pagination);
294
295        let mut reporting_request = ReportingRequest::new(
296            "2020-08-04".parse().unwrap(),
297            "2020-08-14".parse().unwrap(),
298            selector,
299        );
300        reporting_request
301            .set_group_by(vec![CountryOrRegion])
302            .set_time_zone(UTC)
303            .set_return_records_with_no_metrics(true)
304            .set_return_row_totals(true)
305            .set_return_grand_totals(true);
306
307        let value1: Value = serde_json::to_value(reporting_request)?;
308
309        let json_content =
310            include_str!("../../tests/v3/request_body_json_files/get_campaign_level_reports.json");
311        let value2: Value = serde_json::from_str(json_content)?;
312
313        assert_eq!(value1, value2);
314
315        Ok(())
316    }
317
318    #[test]
319    fn test_v3_ser_get_campaign_level_reports_with_granularity() -> Result<(), Box<dyn error::Error>>
320    {
321        let mut pagination = Pagination::new();
322        pagination.set_limit(1000).set_offset(0);
323
324        let mut selector = Selector::new(vec![Sorting::new("countryOrRegion", ASCENDING)]);
325        selector
326            .set_conditions(vec![
327                Condition::new("countriesOrRegions", CONTAINS_ANY, vec!["US", "GB"]),
328                Condition::new("countryOrRegion", IN, vec!["US"]),
329            ])
330            .set_pagination(pagination);
331
332        let mut reporting_request = ReportingRequest::new(
333            "2020-08-04".parse().unwrap(),
334            "2020-08-14".parse().unwrap(),
335            selector,
336        );
337        reporting_request
338            .set_group_by(vec![CountryOrRegion])
339            .set_time_zone(UTC)
340            .set_return_records_with_no_metrics(true)
341            .set_return_row_totals(false)
342            .set_granularity(DAILY)
343            .set_return_grand_totals(false);
344
345        let value1: Value = serde_json::to_value(reporting_request)?;
346
347        let json_content = include_str!(
348            "../../tests/v3/request_body_json_files/get_campaign_level_reports_with_granularity.json",
349        );
350        let value2: Value = serde_json::from_str(json_content)?;
351
352        assert_eq!(value1, value2);
353
354        Ok(())
355    }
356
357    #[test]
358    fn test_v4_de() {
359        match serde_json::from_str::<ReportingRequest>(include_str!("../../tests/v4/request_body_json_files/get_campaign_level_reports_payload_example_1.json")) {
360            Ok(_) => {},
361            Err(err) => panic!("{}", err)
362        }
363
364        match serde_json::from_str::<ReportingRequest>(include_str!("../../tests/v4/request_body_json_files/get_campaign_level_reports_payload_example_2.json")) {
365            Ok(_) => {},
366            Err(err) => panic!("{}", err)
367        }
368
369        match serde_json::from_str::<ReportingRequest>(include_str!("../../tests/v4/request_body_json_files/get_campaign_level_reports_payload_example_3.json")) {
370            Ok(_) => {},
371            Err(err) => panic!("{}", err)
372        }
373    }
374
375    #[test]
376    fn test_v3_de() {
377        match serde_json::from_str::<ReportingRequest>(include_str!("../../tests/v3/request_body_json_files/get_ad_group_level_reports_using_group_by_field_with_geo_values.json")) {
378            Ok(_) => {},
379            Err(err) => panic!("{}", err)
380        }
381
382        match serde_json::from_str::<ReportingRequest>(include_str!(
383            "../../tests/v3/request_body_json_files/get_ad_group_level_reports.json"
384        )) {
385            Ok(_) => {}
386            Err(err) => panic!("{}", err),
387        }
388
389        match serde_json::from_str::<ReportingRequest>(include_str!("../../tests/v3/request_body_json_files/get_campaign_level_reports_with_granularity.json")) {
390            Ok(_) => {},
391            Err(err) => panic!("{}", err)
392        }
393
394        match serde_json::from_str::<ReportingRequest>(include_str!(
395            "../../tests/v3/request_body_json_files/get_campaign_level_reports.json"
396        )) {
397            Ok(_) => {}
398            Err(err) => panic!("{}", err),
399        }
400
401        match serde_json::from_str::<ReportingRequest>(include_str!(
402            "../../tests/v3/request_body_json_files/get_keyword_level_reports.json"
403        )) {
404            Ok(_) => {}
405            Err(err) => panic!("{}", err),
406        }
407
408        match serde_json::from_str::<ReportingRequest>(include_str!(
409            "../../tests/v3/request_body_json_files/get_search_term_level_reports.json"
410        )) {
411            Ok(_) => {}
412            Err(err) => panic!("{}", err),
413        }
414    }
415}