diff --git a/components/ads-client/integration-tests/tests/mars_async.rs b/components/ads-client/integration-tests/tests/mars_async.rs new file mode 100644 index 0000000000..20ad8e1bdf --- /dev/null +++ b/components/ads-client/integration-tests/tests/mars_async.rs @@ -0,0 +1,449 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. +*/ + +use ads_client::{ + worker::{ + DispatchCommand, ErrorOnlyRequestCallback, ImageRequestCallback, SpocRequestCallback, + TileRequestCallback, + }, + MozAdsClient, MozAdsClientApiError, MozAdsClientBuilder, MozAdsEnvironment, MozAdsIABContent, + MozAdsIABContentTaxonomy, MozAdsImage, MozAdsPlacementRequest, MozAdsPlacementRequestWithCount, + MozAdsReportReason, MozAdsRequestOptions, MozAdsSpoc, MozAdsTile, +}; +use std::{ + collections::HashMap, + sync::{ + mpsc::{self, Sender}, + Arc, + }, +}; + +fn init_backend() { + viaduct_hyper::viaduct_init_backend_hyper(); +} + +fn prod_client() -> ads_client::MozAdsClient { + Arc::new(MozAdsClientBuilder::new()) + .environment(MozAdsEnvironment::Prod) + .build() +} + +// Reusable test structure that implements the varying callback traits with generics. +// We can use a generic here, unlike in MAC, because it's not going through uniffi. +pub struct CallbackTestStruct { + on_ad_fn: Box) + Send + Sync>, + on_error_fn: Box) + Send + Sync>, + success_tx: Sender, + err_tx: Sender, +} + +impl CallbackTestStruct { + pub fn new(success_tx: Sender, err_tx: Sender) -> CallbackTestStruct { + CallbackTestStruct { + success_tx, + err_tx, + on_ad_fn: Box::new(|tiles, tx| { + tx.send(tiles) + .expect("Testing channels should not hang up.") + }), + on_error_fn: Box::new(|err, tx| { + tx.send(err).expect("Testing channels should not hang up.") + }), + } + } +} + +impl ErrorOnlyRequestCallback for CallbackTestStruct<(), MozAdsClientApiError> { + fn on_success(&self) { + (self.on_ad_fn)((), self.success_tx.clone()) + } + fn on_error(&self, err: MozAdsClientApiError) { + (self.on_error_fn)(err, self.err_tx.clone()) + } +} + +impl ImageRequestCallback + for CallbackTestStruct, MozAdsClientApiError> +{ + fn on_ad(&self, ads: HashMap) { + (self.on_ad_fn)(ads, self.success_tx.clone()) + } + fn on_error(&self, err: MozAdsClientApiError) { + (self.on_error_fn)(err, self.err_tx.clone()) + } +} + +impl SpocRequestCallback + for CallbackTestStruct>, MozAdsClientApiError> +{ + fn on_ad(&self, ads: HashMap>) { + (self.on_ad_fn)(ads, self.success_tx.clone()) + } + fn on_error(&self, err: MozAdsClientApiError) { + (self.on_error_fn)(err, self.err_tx.clone()) + } +} + +impl TileRequestCallback for CallbackTestStruct, MozAdsClientApiError> { + fn on_ad(&self, tiles: HashMap) { + (self.on_ad_fn)(tiles, self.success_tx.clone()) + } + fn on_error(&self, err: MozAdsClientApiError) { + (self.on_error_fn)(err, self.err_tx.clone()) + } +} + +// Helper function that runs the tile contract test and produces the result (for tests that require this in setup) +// Callback setup in rust can be weighty for tests, so reuse makes a bit more concise +// This should match the logic of `test_contract_tile_prod_async` +fn generate_ad_test(client: &MozAdsClient) -> HashMap { + // Create ad + let (success_tx, success_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + let callback = CallbackTestStruct::, MozAdsClientApiError>::new( + success_tx, err_tx, + ); + + client + .dispatch(DispatchCommand::RequestTileAd { + moz_ad_requests: vec![MozAdsPlacementRequest { + placement_id: "mock_tile_1".to_string(), + iab_content: None, + }], + options: None, + callback: Box::new(callback), + }) + .expect("Asynchronous dispatch should return Ok()"); + + let placements; + loop { + if let Ok(placements_res) = success_rx.recv() { + placements = placements_res; + break; + } + if let Ok(err) = err_rx.recv() { + panic!("Tile ad request failed: {:?}", err); + } + } + + placements +} + +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_contract_image_prod_callback() { + init_backend(); + let client = prod_client(); + + let (success_tx, success_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + let callback = CallbackTestStruct::, MozAdsClientApiError>::new( + success_tx, err_tx, + ); + + client + .dispatch(DispatchCommand::RequestImageAds { + moz_ad_requests: vec![MozAdsPlacementRequest { + iab_content: None, + placement_id: "mock_billboard_1".to_string(), + }], + options: None, + callback: Box::new(callback), + }) + .expect("Asynchronous dispatch should return Ok()"); + + loop { + if let Ok(placements) = success_rx.recv() { + assert!(placements.contains_key("mock_billboard_1")); + break; + } + if let Ok(err) = err_rx.recv() { + panic!("Image ad request failed: {:?}", err); + } + } +} + +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_contract_image_with_categories_prod_callback() { + init_backend(); + let client = prod_client(); + + let (success_tx, success_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + let callback = CallbackTestStruct::, MozAdsClientApiError>::new( + success_tx, err_tx, + ); + + client + .dispatch(DispatchCommand::RequestImageAds { + moz_ad_requests: vec![MozAdsPlacementRequest { + iab_content: Some(MozAdsIABContent { + category_ids: vec!["338".to_string()], + taxonomy: MozAdsIABContentTaxonomy::IAB3_0, + }), + placement_id: "mock_billboard_1".to_string(), + }], + options: Some(MozAdsRequestOptions { + flags: std::collections::HashMap::from([( + "contextual_placement".to_string(), + true, + )]), + ..Default::default() + }), + callback: Box::new(callback), + }) + .expect("Asynchronous dispatch should return Ok()"); + + loop { + if let Ok(placements) = success_rx.recv() { + assert!(placements.contains_key("mock_billboard_1")); + break; + } + if let Ok(err) = err_rx.recv() { + panic!("Image ad request failed: {:?}", err); + } + } +} + +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_contract_spoc_prod_callback() { + init_backend(); + let client = prod_client(); + + let (success_tx, success_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + let callback = + CallbackTestStruct::>, MozAdsClientApiError>::new( + success_tx, err_tx, + ); + + client + .dispatch(DispatchCommand::RequestSpocAds { + moz_ad_requests: vec![MozAdsPlacementRequestWithCount { + count: 3, + iab_content: None, + placement_id: "mock_spoc_1".to_string(), + }], + options: None, + callback: Box::new(callback), + }) + .expect("Asynchronous dispatch should return Ok()"); + + loop { + if let Ok(placements) = success_rx.recv() { + assert!(placements.contains_key("mock_spoc_1")); + assert!(placements.get("mock_spoc_1").unwrap().len() == 3); + + break; + } + if let Ok(err) = err_rx.recv() { + panic!("Spoc ad request failed: {:?}", err); + } + } +} + +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_contract_tile_prod_callback() { + init_backend(); + let client = prod_client(); + + let (success_tx, success_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + let callback = CallbackTestStruct::, MozAdsClientApiError>::new( + success_tx, err_tx, + ); + + client + .dispatch(DispatchCommand::RequestTileAd { + moz_ad_requests: vec![MozAdsPlacementRequest { + iab_content: None, + placement_id: "mock_tile_1".to_string(), + }], + options: None, + callback: Box::new(callback), + }) + .expect("Asynchronous dispatch should return Ok()"); + + loop { + if let Ok(placements) = success_rx.recv() { + assert!(placements.contains_key("mock_tile_1")); + break; + } + if let Ok(err) = err_rx.recv() { + panic!("Tile ad request failed: {:?}", err); + } + } +} + +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_record_impression_callback() { + init_backend(); + let client = prod_client(); + + // Generate tiles + let placements = generate_ad_test(&client); + let ad = placements + .get("mock_tile_1") + .clone() + .expect("mock_tile_1 placement should be present"); + + // Record an impression + let (success_tx, success_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + let callback = CallbackTestStruct::<(), MozAdsClientApiError>::new(success_tx, err_tx); + + client + .dispatch(DispatchCommand::RecordImpression { + impression_url: ad.callbacks.impression.to_string(), + options: None, + callback: Box::new(callback), + }) + .expect("Asynchronous dispatch should return Ok()"); + + loop { + if let Ok(_) = success_rx.recv() { + break; + } + if let Ok(err) = err_rx.recv() { + panic!("record_impression failed: {:?}", err); + } + } +} + +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_record_click_callback() { + init_backend(); + let client = prod_client(); + + // Generate tiles + let placements = generate_ad_test(&client); + let ad = placements + .get("mock_tile_1") + .clone() + .expect("mock_tile_1 placement should be present"); + + // Record a click + let (success_tx, success_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + let callback = CallbackTestStruct::<(), MozAdsClientApiError>::new(success_tx, err_tx); + + client + .dispatch(DispatchCommand::RecordClick { + click_url: ad.callbacks.click.to_string(), + options: None, + callback: Box::new(callback), + }) + .expect("Asynchronous dispatch should return Ok()"); + + loop { + if let Ok(_) = success_rx.recv() { + break; + } + if let Ok(err) = err_rx.recv() { + panic!("record_click failed: {:?}", err); + } + } +} + +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_report_ad_callback() { + init_backend(); + let client = prod_client(); + + // Generate tiles + let placements = generate_ad_test(&client); + let ad = placements + .get("mock_tile_1") + .clone() + .expect("mock_tile_1 placement should be present"); + + // Assertions on the ad + let report_url = ad + .callbacks + .report + .as_ref() + .expect("mock_tile_1 should have a report URL"); + let pairs: Vec<(_, _)> = report_url.query_pairs().collect(); + let placement_id_count = pairs.iter().filter(|(k, _)| k == "placement_id").count(); + let position_count = pairs.iter().filter(|(k, _)| k == "position").count(); + assert_eq!(placement_id_count, 1, "expected exactly one placement_id"); + assert_eq!(position_count, 1, "expected exactly one position"); + + // Report the ad + let (success_tx, success_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + let callback = CallbackTestStruct::<(), MozAdsClientApiError>::new(success_tx, err_tx); + + client + .dispatch(DispatchCommand::ReportAd { + report_url: report_url.to_string(), + reason: MozAdsReportReason::NotInterested, + options: None, + callback: Box::new(callback), + }) + .expect("Asynchronous dispatch should return Ok()"); + + loop { + if let Ok(_) = success_rx.recv() { + break; + } + if let Ok(err) = err_rx.recv() { + panic!("report_ad failed: {:?}", err); + } + } +} + +#[test] +#[ignore = "integration test: run manually with -- --ignored"] +fn test_contract_tile_ohttp_prod_callback() { + init_backend(); + viaduct::ohttp::configure_ohttp_channel( + "ads-client".to_string(), + viaduct::ohttp::OhttpConfig { + relay_url: "https://mozilla-ohttp.fastly-edge.com/".to_string(), + gateway_host: "prod.ohttp-gateway.prod.webservices.mozgcp.net".to_string(), + }, + ) + .expect("OHTTP channel configuration should succeed"); + + let client = prod_client(); + + let (success_tx, success_rx) = mpsc::channel(); + let (err_tx, err_rx) = mpsc::channel(); + let callback = CallbackTestStruct::, MozAdsClientApiError>::new( + success_tx, err_tx, + ); + + client + .dispatch(DispatchCommand::RequestTileAd { + moz_ad_requests: vec![MozAdsPlacementRequest { + placement_id: "mock_tile_1".to_string(), + iab_content: None, + }], + options: None, + callback: Box::new(callback), + }) + .expect("Asynchronous dispatch should return Ok()"); + + loop { + if let Ok(placements) = success_rx.recv() { + assert!( + placements.contains_key("mock_tile_1"), + "OHTTP response should contain mock_tile_1" + ); + + break; + } + if let Ok(err) = err_rx.recv() { + panic!("Tile ad request over OHTTP should succeed: {:?}", err); + } + } +} diff --git a/components/ads-client/src/client/error.rs b/components/ads-client/src/client/error.rs index 2542939493..4d0181f33b 100644 --- a/components/ads-client/src/client/error.rs +++ b/components/ads-client/src/client/error.rs @@ -3,7 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::mars::error::{FetchAdsError, RecordClickError, RecordImpressionError, ReportAdError}; +use std::sync::mpsc::TrySendError; +use crate::{ + mars::error::{FetchAdsError, RecordClickError, RecordImpressionError, ReportAdError}, + worker::DispatchCommand, +}; #[derive(Debug, thiserror::Error)] pub enum ComponentError { @@ -18,6 +22,9 @@ pub enum ComponentError { #[error("Error requesting ads: {0}")] RequestAds(#[from] RequestAdsError), + + #[error("Error dispatching an asynchronous command: {0}")] + Dispatch(TrySendError), } #[derive(Debug, thiserror::Error)] diff --git a/components/ads-client/src/ffi.rs b/components/ads-client/src/ffi.rs index ba50352fdc..eb2eac18b4 100644 --- a/components/ads-client/src/ffi.rs +++ b/components/ads-client/src/ffi.rs @@ -6,7 +6,7 @@ pub mod error; pub mod telemetry; -use std::sync::Arc; +use std::sync::{mpsc, Arc}; use crate::client::config::{AdsCacheConfig, AdsClientConfig}; use crate::client::{AdsClient, ContextIdProvider}; @@ -20,6 +20,7 @@ use crate::mars::ad_response::{ }; use crate::mars::Environment; use crate::mars::ReportReason; +use crate::worker::{ADS_CLIENT_WORKER_CHANNEL_BUFFER_SIZE, DispatchCommand, worker_thread}; use crate::AdsClientUrl; use crate::MozAdsClient; use parking_lot::Mutex; @@ -55,7 +56,7 @@ impl From for Box { } } -#[derive(Default, uniffi::Record)] +#[derive(Default, Clone, Debug, uniffi::Record)] pub struct MozAdsRequestOptions { pub cache_policy: Option, #[uniffi(default)] @@ -64,7 +65,7 @@ pub struct MozAdsRequestOptions { pub ohttp: bool, } -#[derive(Default, uniffi::Record)] +#[derive(Default, Debug, Clone, uniffi::Record)] pub struct MozAdsCallbackOptions { #[uniffi(default = false)] pub ohttp: bool, @@ -138,9 +139,17 @@ impl MozAdsClientBuilder { .map(MozAdsTelemetryWrapper::new) .unwrap_or_else(MozAdsTelemetryWrapper::noop), }; - let client = AdsClient::new(client_config); + let client = Arc::new(Mutex::new(AdsClient::new(client_config))); + let client_clone = client.clone(); + + let (tx, rx) = mpsc::sync_channel::(ADS_CLIENT_WORKER_CHANNEL_BUFFER_SIZE); + + let worker_thread_handle = worker_thread(client_clone, rx); + MozAdsClient { - inner: Mutex::new(client), + inner: client, + _worker_thread_handle: worker_thread_handle, + command_tx: tx, } } diff --git a/components/ads-client/src/ffi/error.rs b/components/ads-client/src/ffi/error.rs index e2da7471c2..cbe367ae2e 100644 --- a/components/ads-client/src/ffi/error.rs +++ b/components/ads-client/src/ffi/error.rs @@ -8,7 +8,7 @@ use error_support::{ErrorHandling, GetErrorHandling}; pub type AdsClientApiResult = std::result::Result; -#[derive(Debug, thiserror::Error, uniffi::Error)] +#[derive(Debug, Clone, thiserror::Error, uniffi::Error)] pub enum MozAdsClientApiError { #[error("Something unexpected occurred.")] Other { reason: String }, @@ -31,3 +31,11 @@ impl GetErrorHandling for ComponentError { }) } } + +impl From for MozAdsClientApiError { + fn from(value: ComponentError) -> Self { + MozAdsClientApiError::Other { + reason: value.to_string(), + } + } +} diff --git a/components/ads-client/src/lib.rs b/components/ads-client/src/lib.rs index 2088ee16cb..90b5e431cb 100644 --- a/components/ads-client/src/lib.rs +++ b/components/ads-client/src/lib.rs @@ -3,7 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use std::collections::HashMap; +use std::{ + collections::HashMap, + sync::{ + mpsc::SyncSender, + Arc, + }, + thread::JoinHandle, +}; use client::error::ComponentError; use error_support::handle_error; @@ -20,11 +27,17 @@ mod ffi; pub mod http_cache; mod mars; pub mod telemetry; +pub mod worker; pub use ffi::*; -use crate::ffi::telemetry::MozAdsTelemetryWrapper; - +use crate::{ + ffi::telemetry::MozAdsTelemetryWrapper, + worker::{ + DispatchCommand, ErrorOnlyRequestCallback, ImageRequestCallback, SpocRequestCallback, + TileRequestCallback, + }, +}; #[cfg(test)] mod test_utils; @@ -38,8 +51,11 @@ uniffi::custom_type!(AdsClientUrl, String, { #[derive(uniffi::Object)] pub struct MozAdsClient { - inner: Mutex>, + inner: Arc>>, + _worker_thread_handle: Option>, + command_tx: SyncSender, } +pub type MozAdsClientInner = Arc>>; #[uniffi::export] impl MozAdsClient { @@ -161,4 +177,111 @@ impl MozAdsClient { .map_err(ComponentError::RequestAds)?; Ok(response.into_iter().map(|(k, v)| (k, v.into())).collect()) } + + #[handle_error(ComponentError)] + #[uniffi::method()] + pub fn dispatch_record_click( + &self, + click_url: String, + options: Option, + callback: Box, + ) -> AdsClientApiResult<()> { + self.dispatch(DispatchCommand::RecordClick { + click_url, + options, + callback, + }) + } + + #[handle_error(ComponentError)] + #[uniffi::method(default(options = None))] + pub fn dispatch_record_impression( + &self, + impression_url: String, + options: Option, + callback: Box, + ) -> AdsClientApiResult<()> { + self.dispatch(DispatchCommand::RecordImpression { + impression_url, + options, + callback, + }) + } + + #[handle_error(ComponentError)] + #[uniffi::method(default(options = None))] + pub fn dispatch_report_ad( + &self, + report_url: String, + reason: MozAdsReportReason, + options: Option, + callback: Box, + ) -> AdsClientApiResult<()> { + self.dispatch(DispatchCommand::ReportAd { + report_url, + reason, + options, + callback, + }) + } + + #[handle_error(ComponentError)] + #[uniffi::method(default(options = None))] + pub fn dispatch_request_image_ads( + &self, + moz_ad_requests: Vec, + options: Option, + callback: Box, + ) -> AdsClientApiResult<()> { + self.dispatch(DispatchCommand::RequestImageAds { + moz_ad_requests, + options, + callback, + }) + } + + #[handle_error(ComponentError)] + #[uniffi::method(default(options = None))] + pub fn dispatch_request_spoc_ads( + &self, + moz_ad_requests: Vec, + options: Option, + callback: Box, + ) -> AdsClientApiResult<()> { + self.dispatch(DispatchCommand::RequestSpocAds { + moz_ad_requests, + options, + callback, + }) + } + + #[handle_error(ComponentError)] + #[uniffi::method(default(options = None))] + pub fn dispatch_request_tile_ads( + &self, + moz_ad_requests: Vec, + options: Option, + callback: Box, + ) -> AdsClientApiResult<()> { + self.dispatch(DispatchCommand::RequestTileAd { + moz_ad_requests, + options, + callback, + }) + } +} + +impl MozAdsClient { + // TODO: The following is the intended surface for dispatching commands with callbacks, but passing an enum with uniffi does not work yet in 0.31. + // When upgraded to 0.32, we can change the myriad #[uniffi::export(callback_interface)] usage to #[uniffi::export(impl = "foreign")], + // and attempt to expose this enum for direct use. + pub fn dispatch(&self, command: DispatchCommand) -> Result<(), ComponentError> { + // try_send provides an error on a disconnection or a SyncChannel full buffer + // TODO: drop oldest on error, not newest + // TODO: This currently simply drops messages if the thread is killed for whatever reason- should we fall through to sync? + self.command_tx + .try_send(command) + .map_err(ComponentError::Dispatch)?; + Ok(()) + } } diff --git a/components/ads-client/src/worker.rs b/components/ads-client/src/worker.rs new file mode 100644 index 0000000000..8db4c5ce0e --- /dev/null +++ b/components/ads-client/src/worker.rs @@ -0,0 +1,251 @@ +use crate::{ + client::error::ComponentError, + http_cache::CachePolicy, + mars::{ + ad_request::{AdPlacementRequest, AdRequestFlags}, + ad_response::AdSpoc, + error::CallbackRequestError, + }, + AdsClientApiResult, MozAdsCallbackOptions, MozAdsClientApiError, MozAdsClientInner, + MozAdsImage, MozAdsPlacementRequest, MozAdsPlacementRequestWithCount, MozAdsReportReason, + MozAdsRequestOptions, MozAdsSpoc, MozAdsTile, +}; +use error_support::handle_error; +use std::{collections::HashMap, sync::mpsc::Receiver, thread::JoinHandle}; +use url::Url as AdsClientUrl; + +pub const ADS_CLIENT_WORKER_CHANNEL_BUFFER_SIZE: usize = 1000; +pub const ADS_CLIENT_WORKER_THREAD_NAME : &'static str = "ads-client.worker"; + +// Spawn worker thread with reference to client and dispatch command receiver. +// Returns None if thread fails to build. +pub fn worker_thread(inner_client: MozAdsClientInner, rx: Receiver) -> Option> { + let worker_thread_handle = std::thread::Builder::new() + .name(ADS_CLIENT_WORKER_THREAD_NAME.to_string()) + .spawn(move || crate::worker::worker(inner_client, rx)).inspect_err(|err| { + error_support::error!("Failed to create ads-client worker thread `{ADS_CLIENT_WORKER_THREAD_NAME}` with: {err}") + }).ok()?; + Some(worker_thread_handle) +} + +fn worker(inner_client: MozAdsClientInner, rx: Receiver) { + while let Ok(task) = rx.recv() { + + // Synchronously run tasks in the order they are passed in this separate channel. + let task: DispatchCommand = task; + task.run_command(&inner_client) + .expect("TODO: handle error here. maybe retries should be here?"); + } +} + +pub enum DispatchCommand { + RecordClick { + click_url: String, + options: Option, + callback: Box, + }, + RecordImpression { + impression_url: String, + options: Option, + callback: Box, + }, + ReportAd { + report_url: String, + reason: MozAdsReportReason, + options: Option, + callback: Box, + }, + RequestImageAds { + moz_ad_requests: Vec, + options: Option, + callback: Box, + }, + RequestSpocAds { + moz_ad_requests: Vec, + options: Option, + callback: Box, + }, + RequestTileAd { + moz_ad_requests: Vec, + options: Option, + callback: Box, + }, +} + +impl DispatchCommand { + #[handle_error(ComponentError)] + pub fn run_command(self, ads_client_inner: &MozAdsClientInner) -> AdsClientApiResult<()> { + // TODO: Duplicated behavior with the sync functions. Behavior is pretty simple though- probably OK to duplicate. + match self { + DispatchCommand::RecordClick { + click_url, + options, + callback, + } => { + let resp = (|| { + let url = AdsClientUrl::parse(&click_url).map_err(|e| { + ComponentError::RecordClick(CallbackRequestError::InvalidUrl(e).into()) + })?; + let ohttp = options.map(|o| o.ohttp).unwrap_or(false); + let inner = ads_client_inner.lock(); + inner + .record_click(url, ohttp) + .map_err(ComponentError::RecordClick) + })(); + + match resp { + Ok(_) => callback.on_success(), + Err(e) => callback.on_error(e.into()), + } + } + DispatchCommand::RecordImpression { + impression_url, + options, + callback, + } => { + let resp = (|| { + let url = AdsClientUrl::parse(&impression_url).map_err(|e| { + ComponentError::RecordImpression(CallbackRequestError::InvalidUrl(e).into()) + })?; + let ohttp = options.map(|o| o.ohttp).unwrap_or(false); + let inner = ads_client_inner.lock(); + inner + .record_impression(url, ohttp) + .map_err(ComponentError::RecordImpression) + })(); + + match resp { + Ok(_) => callback.on_success(), + Err(e) => callback.on_error(e.into()), + } + } + DispatchCommand::ReportAd { + report_url, + reason, + options, + callback, + } => { + let resp = (|| { + let url = AdsClientUrl::parse(&report_url).map_err(|e| { + ComponentError::ReportAd(CallbackRequestError::InvalidUrl(e).into()) + })?; + let ohttp = options.map(|o| o.ohttp).unwrap_or(false); + let inner = ads_client_inner.lock(); + inner + .report_ad(url, reason.into(), ohttp) + .map_err(ComponentError::ReportAd) + })(); + + match resp { + Ok(_) => callback.on_success(), + Err(e) => callback.on_error(e.into()), + } + } + DispatchCommand::RequestImageAds { + moz_ad_requests, + options, + callback, + } => { + let resp = (|| { + let inner = ads_client_inner.lock(); + let requests: Vec = + moz_ad_requests.iter().map(|r| r.into()).collect(); + let options = options.unwrap_or_default(); + let flags = AdRequestFlags::from(&options); + let ohttp = options.ohttp; + let cache_policy: CachePolicy = options.into(); + let response = inner + .request_image_ads(requests, flags, Some(cache_policy), ohttp) + .map_err(ComponentError::RequestAds)?; + Ok(response.into_iter().map(|(k, v)| (k, v.into())).collect()) + })(); + match resp { + Ok(ads) => callback.on_ad(ads), + Err(e) => callback.on_error(e), + } + } + + DispatchCommand::RequestSpocAds { + moz_ad_requests, + options, + callback, + } => { + let resp = (|| { + let inner = ads_client_inner.lock(); + let requests: Vec = + moz_ad_requests.iter().map(|r| r.into()).collect(); + let options = options.unwrap_or_default(); + let flags = AdRequestFlags::from(&options); + let ohttp = options.ohttp; + let cache_policy: CachePolicy = options.into(); + let response: HashMap> = inner + .request_spoc_ads(requests, flags, Some(cache_policy), ohttp) + .map_err(ComponentError::RequestAds)?; + Ok::<_, ComponentError>( + response + .into_iter() + .map(|(k, v)| (k, v.into_iter().map(|spoc| spoc.into()).collect())) + .collect(), + ) + })(); + match resp { + Ok(ads) => callback.on_ad(ads), + Err(e) => callback.on_error(e.into()), + } + } + + DispatchCommand::RequestTileAd { + moz_ad_requests, + options, + callback, + } => { + let resp = (|| { + let inner = ads_client_inner.lock(); + let requests: Vec = + moz_ad_requests.iter().map(|r| r.into()).collect(); + let options = options.unwrap_or_default(); + let flags = AdRequestFlags::from(&options); + let ohttp = options.ohttp; + let cache_policy: CachePolicy = options.into(); + let response = inner + .request_tile_ads(requests, flags, Some(cache_policy), ohttp) + .map_err(ComponentError::RequestAds)?; + Ok(response.into_iter().map(|(k, v)| (k, v.into())).collect()) + })(); + match resp { + Ok(tiles) => callback.on_ad(tiles), + Err(e) => callback.on_error(e), + } + } + } + Ok(()) + } +} + +// TODO: Uniffi does not currently support generics here or direct function callbacks, so we use a few distinct interfaces. +// TODO: We use #[uniffi::export(callback_interface)] rather than #[uniffi::export(with_foreign)] for reasons discussed here: +// - https://github.com/mozilla/application-services/pull/7443 +// In the future, this should be changed to uniffi::export(impl = "foreign") alongside the enum change, when uniffi = 0.32 +#[uniffi::export(callback_interface)] +pub trait ErrorOnlyRequestCallback: Send + Sync { + fn on_success(&self); + fn on_error(&self, err: MozAdsClientApiError); +} + +#[uniffi::export(callback_interface)] +pub trait ImageRequestCallback: Send + Sync { + fn on_ad(&self, ads: HashMap); + fn on_error(&self, err: MozAdsClientApiError); +} + +#[uniffi::export(callback_interface)] +pub trait SpocRequestCallback: Send + Sync { + fn on_ad(&self, ads: HashMap>); + fn on_error(&self, err: MozAdsClientApiError); +} + +#[uniffi::export(callback_interface)] +pub trait TileRequestCallback: Send + Sync { + fn on_ad(&self, tiles: HashMap); + fn on_error(&self, err: MozAdsClientApiError); +}