1use std::collections::HashMap;
7
8use serde::{Deserialize, Deserializer, Serialize};
9
10fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
11where
12 D: Deserializer<'de>,
13{
14 let opt = Option::<String>::deserialize(deserializer)?;
15 Ok(match opt {
16 Some(s) if s.trim().is_empty() => None,
17 other => other,
18 })
19}
20
21#[derive(Debug, Serialize, Deserialize, PartialEq, uniffi::Record)]
22pub struct AdPlacementRequest {
23 pub placement: String,
24 pub count: u32,
25 pub content: Option<AdContentCategory>,
26}
27
28#[derive(Debug, Serialize, PartialEq, Deserialize, uniffi::Enum)]
29pub enum IABAdUnitFormat {
30 Billboard,
31 SmartphoneBanner300,
32 SmartphoneBanner320,
33 Leaderboard,
34 SuperLeaderboardPushdown,
35 Portrait,
36 Skyscraper,
37 MediumRectangle,
38 TwentyBySixty,
39 MobilePhoneInterstitial640,
40 MobilePhoneInterstitial750,
41 MobilePhoneInterstitial1080,
42 FeaturePhoneSmallBanner,
43 FeaturePhoneMediumBanner,
44 FeaturePhoneLargeBanner,
45}
46
47#[derive(Clone, Copy, Debug, Serialize, PartialEq, Deserialize, uniffi::Enum)]
48pub enum IABContentTaxonomy {
49 #[serde(rename = "IAB-1.0")]
50 IAB1_0,
51
52 #[serde(rename = "IAB-2.0")]
53 IAB2_0,
54
55 #[serde(rename = "IAB-2.1")]
56 IAB2_1,
57
58 #[serde(rename = "IAB-2.2")]
59 IAB2_2,
60
61 #[serde(rename = "IAB-3.0")]
62 IAB3_0,
63}
64
65#[derive(Debug, Serialize, Deserialize, PartialEq, uniffi::Record)]
66pub struct AdContentCategory {
67 pub taxonomy: IABContentTaxonomy,
68 pub categories: Vec<String>,
69}
70
71#[derive(Debug, Serialize, Deserialize, PartialEq, uniffi::Record)]
72pub struct AdRequest {
73 pub context_id: String,
74 pub placements: Vec<AdPlacementRequest>,
75}
76
77#[derive(Debug, PartialEq, Serialize, Deserialize, uniffi::Record)]
78pub struct AdCallbacks {
79 #[serde(default, deserialize_with = "empty_string_as_none")]
80 pub click: Option<String>,
81 #[serde(default, deserialize_with = "empty_string_as_none")]
82 pub impression: Option<String>,
83 #[serde(default, deserialize_with = "empty_string_as_none")]
84 pub report: Option<String>,
85}
86
87#[derive(Debug, PartialEq, Serialize, Deserialize, uniffi::Record)]
88pub struct MozAd {
89 pub alt_text: Option<String>,
90 pub block_key: Option<String>,
91 pub callbacks: Option<AdCallbacks>,
92 pub format: Option<String>,
93 pub image_url: Option<String>, pub url: Option<String>,
95}
96
97#[derive(Debug, PartialEq, Serialize, Deserialize, uniffi::Record)]
98pub struct AdResponse {
99 #[serde(flatten)]
100 pub data: HashMap<String, Vec<MozAd>>,
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use serde_json::{from_str, json, to_value};
107
108 #[test]
109 fn test_ad_placement_request_with_content_serialize() {
110 let request = AdPlacementRequest {
111 placement: "example_placement".into(),
112 count: 1,
113 content: Some(AdContentCategory {
114 taxonomy: IABContentTaxonomy::IAB2_1,
115 categories: vec!["Technology".into(), "Programming".into()],
116 }),
117 };
118
119 let serialized = to_value(&request).unwrap();
120
121 let expected_json = json!({
122 "placement": "example_placement",
123 "count": 1,
124 "content": {
125 "taxonomy": "IAB-2.1",
126 "categories": ["Technology", "Programming"]
127 }
128 });
129
130 assert_eq!(serialized, expected_json);
131 }
132
133 #[test]
134 fn test_iab_content_taxonomy_serialize() {
135 use serde_json::to_string;
136
137 let s = to_string(&IABContentTaxonomy::IAB1_0).unwrap();
139 assert_eq!(s, "\"IAB-1.0\"");
140
141 let s = to_string(&IABContentTaxonomy::IAB2_0).unwrap();
142 assert_eq!(s, "\"IAB-2.0\"");
143
144 let s = to_string(&IABContentTaxonomy::IAB2_1).unwrap();
145 assert_eq!(s, "\"IAB-2.1\"");
146
147 let s = to_string(&IABContentTaxonomy::IAB2_2).unwrap();
148 assert_eq!(s, "\"IAB-2.2\"");
149
150 let s = to_string(&IABContentTaxonomy::IAB3_0).unwrap();
151 assert_eq!(s, "\"IAB-3.0\"");
152 }
153
154 #[test]
155 fn test_ad_callbacks_empty_and_missing_to_none() {
156 let j = json!({
158 "click": "",
159 "impression": " ",
160 })
162 .to_string();
163
164 let got: AdCallbacks = from_str(&j).unwrap();
165 assert_eq!(
166 got,
167 AdCallbacks {
168 click: None,
169 impression: None,
170 report: None
171 }
172 );
173 }
174
175 #[test]
176 fn test_moz_ad_full() {
177 let response_full = json!({
178 "alt_text": "An ad for an anvil",
179 "block_key": "abc123",
180 "callbacks": {
181 "click": "https://buyanvilseveryday.test/click",
182 "impression": "https://buyanvilseveryday.test/impression",
183 "report": "https://buyanvilseveryday.test/report"
184 },
185 "format": "Leaderboard",
186 "image_url": "https://buyanvilseveryday.test/img.png",
187 "url": "https://buyanvilseveryday.test"
188 })
189 .to_string();
190
191 let full: MozAd = from_str(&response_full).unwrap();
192 assert_eq!(
193 full,
194 MozAd {
195 alt_text: Some("An ad for an anvil".into()),
196 block_key: Some("abc123".into()),
197 callbacks: Some(AdCallbacks {
198 click: Some("https://buyanvilseveryday.test/click".into()),
199 impression: Some("https://buyanvilseveryday.test/impression".into()),
200 report: Some("https://buyanvilseveryday.test/report".into()),
201 }),
202 format: Some("Leaderboard".into()),
203 image_url: Some("https://buyanvilseveryday.test/img.png".into()),
204 url: Some("https://buyanvilseveryday.test".into()),
205 }
206 );
207 }
208
209 #[test]
210 fn test_moz_ad_response_partial() {
211 let response_partial = json!({
212 "alt_text": null,
213 "callbacks": {
214 "click": "",
215 "impression": " ",
216 "report": null
217 }
218 })
219 .to_string();
220
221 let partial: MozAd = from_str(&response_partial).unwrap();
222 assert_eq!(
223 partial,
224 MozAd {
225 alt_text: None,
226 block_key: None,
227 callbacks: Some(AdCallbacks {
228 click: None,
229 impression: None,
230 report: None,
231 }),
232 format: None,
233 image_url: None,
234 url: None,
235 }
236 );
237 }
238
239 #[test]
240 fn test_ad_response_serialization() {
241 let raw_ad_response = json!({
242 "example_placement_1": [
243 {
244 "url": "https://ads.fakeexample.org/example_ad_1",
245 "image_url": "https://ads.fakeexample.org/example_image_1",
246 "format": "billboard",
247 "alt_text": "An ad for a puppy",
248 "callbacks": {
249 "click": "https://ads.fakeexample.org/click/example_ad_1",
250 "report": "https://ads.fakeexample.org/report/example_ad_1"
252 }
253 }
254 ],
255 "example_placement_2": [
256 {
257 "url": "https://ads.fakeexample.org/example_ad_2",
258 "image_url": "https://ads.fakeexample.org/example_image_2",
259 "format": "skyscraper",
260 "alt_text": "An ad for a pet duck",
261 "callbacks": {
262 "click": "https://ads.fakeexample.org/click/example_ad_2",
263 "impression": "https://ads.fakeexample.org/impression/example_ad_2",
264 "report": "https://ads.fakeexample.org/report/example_ad_2"
265 }
266 }
267 ]
268 })
269 .to_string();
270
271 let parsed: AdResponse = from_str(&raw_ad_response).unwrap();
272
273 let expected = AdResponse {
274 data: HashMap::from([
275 (
276 "example_placement_1".to_string(),
277 vec![MozAd {
278 url: Some("https://ads.fakeexample.org/example_ad_1".to_string()),
279 image_url: Some("https://ads.fakeexample.org/example_image_1".to_string()),
280 format: Some("billboard".to_string()),
281 block_key: None,
282 alt_text: Some("An ad for a puppy".to_string()),
283 callbacks: Some(AdCallbacks {
284 click: Some(
285 "https://ads.fakeexample.org/click/example_ad_1".to_string(),
286 ),
287 impression: None, report: Some(
289 "https://ads.fakeexample.org/report/example_ad_1".to_string(),
290 ),
291 }),
292 }],
293 ),
294 (
295 "example_placement_2".to_string(),
296 vec![MozAd {
297 url: Some("https://ads.fakeexample.org/example_ad_2".to_string()),
298 image_url: Some("https://ads.fakeexample.org/example_image_2".to_string()),
299 format: Some("skyscraper".to_string()),
300 block_key: None,
301 alt_text: Some("An ad for a pet duck".to_string()),
302 callbacks: Some(AdCallbacks {
303 click: Some(
304 "https://ads.fakeexample.org/click/example_ad_2".to_string(),
305 ),
306 impression: Some(
307 "https://ads.fakeexample.org/impression/example_ad_2".to_string(),
308 ),
309 report: Some(
310 "https://ads.fakeexample.org/report/example_ad_2".to_string(),
311 ),
312 }),
313 }],
314 ),
315 ]),
316 };
317
318 assert_eq!(parsed, expected);
319 }
320
321 #[test]
322 fn test_empty_ad_response_serialization() {
323 let raw_ad_response = json!({
324 "example_placement_1": [],
325 "example_placement_2": []
326 })
327 .to_string();
328
329 let parsed: AdResponse = from_str(&raw_ad_response).unwrap();
330
331 let expected = AdResponse {
332 data: HashMap::from([
333 ("example_placement_1".to_string(), vec![]),
334 ("example_placement_2".to_string(), vec![]),
335 ]),
336 };
337
338 assert_eq!(parsed, expected);
339 }
340}