Skip to content

Commit 7608277

Browse files
authored
Refactor ads-client to use Url type for callback URLs (#7038)
* refactor(ads-client): deserialize callbacks response as Url * test(ads-client): click and impression callback urls
1 parent 6d2abb8 commit 7608277

File tree

7 files changed

+197
-199
lines changed

7 files changed

+197
-199
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
- The `context_id` is now generated and rotated via the existing eponym component crate.
3535
- Added request caching mechanism using SQLite with configurable TTL and max size.
3636
- Added configuration options for the cache.
37+
- Deserialize callbacks with `url::Url`
3738

3839
### Relay
3940
- **⚠️ Breaking Change:** The error handling for the Relay component has been refactored for stronger forward compatibility and more transparent error reporting in Swift and Kotlin via UniFFI.

components/ads-client/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ serde_json = "1"
2424
thiserror = "2"
2525
once_cell = "1.5"
2626
uniffi = { version = "0.29.0" }
27-
url = "2"
27+
url = { version = "2", features = ["serde"] }
2828
uuid = { version = "1.3", features = ["v4"] }
2929
viaduct = { path = "../viaduct" }
3030
sql-support = { path = "../support/sql" }

components/ads-client/src/error.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,20 +109,17 @@ pub enum EmitTelemetryError {
109109

110110
#[derive(Debug, thiserror::Error)]
111111
pub enum CallbackRequestError {
112-
#[error("URL parse error: {0}")]
113-
UrlParse(#[from] url::ParseError),
114-
115-
#[error("Error sending request: {0}")]
116-
Request(#[from] viaduct::ViaductError),
112+
#[error("Could not fetch ads, MARS responded with: {0}")]
113+
HTTPError(#[from] HTTPError),
117114

118115
#[error("JSON error: {0}")]
119116
Json(#[from] serde_json::Error),
120117

121-
#[error("Could not fetch ads, MARS responded with: {0}")]
122-
HTTPError(#[from] HTTPError),
123-
124118
#[error("Callback URL missing: {message}")]
125119
MissingCallback { message: String },
120+
121+
#[error("Error sending request: {0}")]
122+
Request(#[from] viaduct::ViaductError),
126123
}
127124

128125
#[derive(Debug, thiserror::Error)]

components/ads-client/src/lib.rs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ use instrument::TrackError;
1717
use mars::{DefaultMARSClient, MARSClient};
1818
use models::{AdContentCategory, AdRequest, AdResponse, IABContentTaxonomy, MozAd};
1919
use parking_lot::Mutex;
20+
use url::Url as AdsClientUrl;
2021
use uuid::Uuid;
2122

22-
use crate::error::AdsClientApiError;
23+
use crate::error::{AdsClientApiError, CallbackRequestError};
2324
use crate::http_cache::{ByteSize, CacheMode, HttpCacheError, RequestCachePolicy};
2425

2526
mod error;
@@ -33,6 +34,12 @@ mod test_utils;
3334

3435
uniffi::setup_scaffolding!("ads_client");
3536

37+
uniffi::custom_type!(AdsClientUrl, String, {
38+
remote,
39+
try_lift: |val| Ok(AdsClientUrl::parse(&val)?),
40+
lower: |obj| obj.as_str().to_string(),
41+
});
42+
3643
const DEFAULT_TTL_SECONDS: u64 = 300;
3744
const DEFAULT_MAX_CACHE_SIZE_MIB: u64 = 10;
3845

@@ -200,23 +207,28 @@ impl MozAdsClientInner {
200207
}
201208

202209
fn record_impression(&self, placement: &MozAdsPlacement) -> Result<(), RecordImpressionError> {
203-
let impression_callback = placement.content.callbacks.impression.clone();
204-
205-
self.client.record_impression(impression_callback)?;
206-
Ok(())
210+
self.client
211+
.record_impression(placement.content.callbacks.impression.clone())
207212
}
208213

209214
fn record_click(&self, placement: &MozAdsPlacement) -> Result<(), RecordClickError> {
210-
let click_callback = placement.content.callbacks.click.clone();
211-
212-
self.client.record_click(click_callback)?;
213-
Ok(())
215+
self.client
216+
.record_click(placement.content.callbacks.click.clone())
214217
}
215218

216219
fn report_ad(&self, placement: &MozAdsPlacement) -> Result<(), ReportAdError> {
217220
let report_ad_callback = placement.content.callbacks.report.clone();
218221

219-
self.client.report_ad(report_ad_callback)?;
222+
match report_ad_callback {
223+
Some(callback) => self.client.report_ad(callback)?,
224+
None => {
225+
return Err(ReportAdError::CallbackRequest(
226+
CallbackRequestError::MissingCallback {
227+
message: "Report callback url empty.".to_string(),
228+
},
229+
));
230+
}
231+
}
220232
Ok(())
221233
}
222234

@@ -582,11 +594,16 @@ mod tests {
582594
block_key: "abc123".into(),
583595
alt_text: Some("An ad for a pet dragon".to_string()),
584596
callbacks: AdCallbacks {
585-
click: Some("https://ads.fakeexample.org/click/example_ad_2_2".to_string()),
586-
impression: Some(
587-
"https://ads.fakeexample.org/impression/example_ad_2_2".to_string(),
597+
click: AdsClientUrl::parse("https://ads.fakeexample.org/click/example_ad_2_2")
598+
.unwrap(),
599+
impression: AdsClientUrl::parse(
600+
"https://ads.fakeexample.org/impression/example_ad_2_2",
601+
)
602+
.unwrap(),
603+
report: Some(
604+
AdsClientUrl::parse("https://ads.fakeexample.org/report/example_ad_2_2")
605+
.unwrap(),
588606
),
589-
report: Some("https://ads.fakeexample.org/report/example_ad_2_2".to_string()),
590607
},
591608
});
592609

components/ads-client/src/mars.rs

Lines changed: 25 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,9 @@ pub trait MARSClient: Sync + Send {
4141
request: &AdRequest,
4242
cache_policy: &RequestCachePolicy,
4343
) -> Result<AdResponse, FetchAdsError>;
44-
fn record_impression(
45-
&self,
46-
url_callback_string: Option<String>,
47-
) -> Result<(), RecordImpressionError>;
48-
fn record_click(&self, url_callback_string: Option<String>) -> Result<(), RecordClickError>;
49-
fn report_ad(&self, url_callback_string: Option<String>) -> Result<(), ReportAdError>;
44+
fn record_impression(&self, callback: Url) -> Result<(), RecordImpressionError>;
45+
fn record_click(&self, callback: Url) -> Result<(), RecordClickError>;
46+
fn report_ad(&self, callback: Url) -> Result<(), ReportAdError>;
5047
fn get_context_id(&self) -> context_id::ApiResult<String>;
5148
fn cycle_context_id(&mut self) -> context_id::ApiResult<String>;
5249
fn get_mars_endpoint(&self) -> &Url;
@@ -97,9 +94,8 @@ impl DefaultMARSClient {
9794
}
9895
}
9996

100-
fn make_callback_request(&self, url_callback_string: &str) -> Result<(), CallbackRequestError> {
101-
let url = Url::parse(url_callback_string)?;
102-
let request = Request::get(url);
97+
fn make_callback_request(&self, callback: Url) -> Result<(), CallbackRequestError> {
98+
let request = Request::get(callback);
10399
let response = request.send()?;
104100
check_http_status_for_error(&response).map_err(Into::into)
105101
}
@@ -153,37 +149,16 @@ impl MARSClient for DefaultMARSClient {
153149
}
154150
}
155151

156-
fn record_impression(
157-
&self,
158-
url_callback_string: Option<String>,
159-
) -> Result<(), RecordImpressionError> {
160-
match url_callback_string {
161-
Some(callback) => self.make_callback_request(&callback).map_err(Into::into),
162-
None => Err(CallbackRequestError::MissingCallback {
163-
message: "Impression callback url empty.".to_string(),
164-
}
165-
.into()),
166-
}
152+
fn record_impression(&self, callback: Url) -> Result<(), RecordImpressionError> {
153+
Ok(self.make_callback_request(callback)?)
167154
}
168155

169-
fn record_click(&self, url_callback_string: Option<String>) -> Result<(), RecordClickError> {
170-
match url_callback_string {
171-
Some(callback) => self.make_callback_request(&callback).map_err(Into::into),
172-
None => Err(CallbackRequestError::MissingCallback {
173-
message: "Click callback url empty.".to_string(),
174-
}
175-
.into()),
176-
}
156+
fn record_click(&self, callback: Url) -> Result<(), RecordClickError> {
157+
Ok(self.make_callback_request(callback)?)
177158
}
178159

179-
fn report_ad(&self, url_callback_string: Option<String>) -> Result<(), ReportAdError> {
180-
match url_callback_string {
181-
Some(callback) => self.make_callback_request(&callback).map_err(Into::into),
182-
None => Err(CallbackRequestError::MissingCallback {
183-
message: "Report callback url empty.".to_string(),
184-
}
185-
.into()),
186-
}
160+
fn report_ad(&self, callback: Url) -> Result<(), ReportAdError> {
161+
Ok(self.make_callback_request(callback)?)
187162
}
188163

189164
fn clear_cache(&self) -> Result<(), HttpCacheError> {
@@ -225,36 +200,19 @@ mod tests {
225200
assert_ne!(client.get_context_id().unwrap(), TEST_CONTEXT_ID);
226201
}
227202

228-
#[test]
229-
fn test_record_impression_with_empty_callback_should_fail() {
230-
let client = create_test_client(mockito::server_url());
231-
let result = client.record_impression(None);
232-
assert!(result.is_err());
233-
}
234-
235-
#[test]
236-
fn test_record_click_with_empty_callback_should_fail() {
237-
let client = create_test_client(mockito::server_url());
238-
let result = client.record_click(None);
239-
assert!(result.is_err());
240-
}
241-
242-
#[test]
243-
fn test_record_report_with_empty_callback_should_fail() {
244-
let client = create_test_client(mockito::server_url());
245-
let result = client.report_ad(None);
246-
assert!(result.is_err());
247-
}
248-
249203
#[test]
250204
fn test_record_impression_with_valid_url_should_succeed() {
251205
viaduct_dev::init_backend_dev();
252206
let _m = mock("GET", "/impression_callback_url")
253207
.with_status(200)
254208
.create();
255209
let client = create_test_client(mockito::server_url());
256-
let url = format!("{}/impression_callback_url", &mockito::server_url());
257-
let result = client.record_impression(Some(url));
210+
let url = Url::parse(&format!(
211+
"{}/impression_callback_url",
212+
&mockito::server_url()
213+
))
214+
.unwrap();
215+
let result = client.record_impression(url);
258216
assert!(result.is_ok());
259217
}
260218

@@ -264,8 +222,8 @@ mod tests {
264222
let _m = mock("GET", "/click_callback_url").with_status(200).create();
265223

266224
let client = create_test_client(mockito::server_url());
267-
let url = format!("{}/click_callback_url", &mockito::server_url());
268-
let result = client.record_click(Some(url));
225+
let url = Url::parse(&format!("{}/click_callback_url", &mockito::server_url())).unwrap();
226+
let result = client.record_click(url);
269227
assert!(result.is_ok());
270228
}
271229

@@ -277,8 +235,12 @@ mod tests {
277235
.create();
278236

279237
let client = create_test_client(mockito::server_url());
280-
let url = format!("{}/report_ad_callback_url", &mockito::server_url());
281-
let result = client.report_ad(Some(url));
238+
let url = Url::parse(&format!(
239+
"{}/report_ad_callback_url",
240+
&mockito::server_url()
241+
))
242+
.unwrap();
243+
let result = client.report_ad(url);
282244
assert!(result.is_ok());
283245
}
284246

0 commit comments

Comments
 (0)