Skip to content

Commit 72f76f9

Browse files
authored
refactor(ads-client): convert MozAdsClientBuilder to fluent builder pattern with Arc-based setters (#7188)
- Restructure MozAdsClientBuilder to use Mutex<Inner> pattern following SuggestStoreBuilder - Change build() method from consuming self to borrowing &self for uniffi Arc compatibility - Add fluent setter methods: environment(), cache_config(), telemetry() - Fix uniffi error where Arc<T> cannot move values out with consuming self - Add Default impl for MozAdsClientBuilder and clippy allow for new_ret_no_self - Update usage.md with new builder pattern examples for Swift and Kotlin - Update integration tests to use new builder.build() pattern - Remove unused AdsClientConfig import
1 parent d11a251 commit 72f76f9

File tree

5 files changed

+144
-64
lines changed

5 files changed

+144
-64
lines changed

components/ads-client/ARCHITECTURE.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This component uses a clear separation between **FFI (Foreign Function Interface
1313

1414
#### 1. Clear Public API Identification
1515

16-
All types prefixed with `MozAds` (e.g., `MozAdsClient`, `MozAd`, `MozAdsClientConfig`) represent the **public API contract**. This makes it immediately obvious:
16+
All types prefixed with `MozAds` (e.g., `MozAdsClient`, `MozAd`, `MozAdsClientBuilder`) represent the **public API contract**. This makes it immediately obvious:
1717

1818
- What types external consumers depend on
1919
- What changes require coordination with consumers
@@ -40,7 +40,38 @@ The separation enables future API versioning strategies:
4040
- Evolve the public API independently from internal implementation
4141
- Provide migration paths between API versions
4242

43-
### Implementation Pattern
43+
### Implementation Patterns
44+
45+
#### Builder Pattern for Client Construction
46+
47+
`MozAdsClientBuilder` follows the fluent builder pattern compatible with UniFFI's Arc-based object model:
48+
49+
```rust
50+
#[derive(uniffi::Object)]
51+
pub struct MozAdsClientBuilder(Mutex<MozAdsClientBuilderInner>);
52+
53+
#[uniffi::export]
54+
impl MozAdsClientBuilder {
55+
// Setter methods take Arc<Self> and return Arc<Self> for chaining
56+
pub fn environment(self: Arc<Self>, environment: MozAdsEnvironment) -> Arc<Self> {
57+
self.0.lock().environment = Some(environment);
58+
self // Returns self for method chaining
59+
}
60+
61+
// build() takes &self (not consuming) to work with UniFFI's Arc wrapping
62+
pub fn build(&self) -> MozAdsClient { ... }
63+
}
64+
```
65+
66+
Key design decisions:
67+
- **Mutex wrapper**: Enables interior mutability across FFI boundaries
68+
- **Arc<Self> for setters**: Required for UniFFI objects, enables method chaining
69+
- **&self for build()**: UniFFI wraps objects in Arc, so we can't consume self
70+
- **Separate inner struct**: Keeps the actual configuration fields in a non-uniffi type
71+
72+
This pattern is consistent with other builders in the codebase (e.g., `SuggestStoreBuilder`).
73+
74+
#### Type Conversions
4475

4576
The conversion between FFI and business logic types is handled through `From`/`Into` trait implementations in `ffi.rs`:
4677

components/ads-client/docs/usage.md

Lines changed: 52 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,35 @@ pub struct MozAdsClient {
1717
}
1818
```
1919

20-
#### Constructors
20+
#### Creating a Client
2121

22-
```rust
23-
impl MozAdsClient {
24-
pub fn new(client_config: Option<MozAdsClientConfig>) -> Self
25-
}
22+
Use the `MozAdsClientBuilder` to configure and create the client. The builder provides a fluent API for setting configuration options.
23+
24+
**Swift:**
25+
```swift
26+
let client = MozAdsClientBuilder()
27+
.environment(environment: .prod)
28+
.cacheConfig(cacheConfig: cache)
29+
.telemetry(telemetry: telemetry)
30+
.build()
2631
```
2732

28-
Creates a new ads client with an optional configuration object.
29-
If a cache configuration is provided, the client will initialize an on-disk HTTP cache at the given path.
33+
**Kotlin:**
34+
```kotlin
35+
val client = MozAdsClientBuilder()
36+
.environment(MozAdsEnvironment.PROD)
37+
.cacheConfig(cache)
38+
.telemetry(telemetry)
39+
.build()
40+
```
41+
42+
**Rust:**
43+
```rust
44+
let client = MozAdsClientBuilder::new()
45+
.environment(MozAdsEnvironment::Prod)
46+
.cache_config(cache_config)
47+
.build();
48+
```
3049

3150
#### Methods
3251

@@ -45,34 +64,38 @@ If a cache configuration is provided, the client will initialize an on-disk HTTP
4564
>
4665
> - We recommend that this client be initialized as a singleton or something similar so that multiple instances of the client do not exist at once.
4766
> - Responses omit placements with no fill. Empty placements do not appear in the returned maps.
48-
> - The HTTP cache is internally managed. Configuration can be set with `MozAdsClientConfig`. Per-request cache settings can be set with `MozAdsRequestOptions`.
67+
> - The HTTP cache is internally managed. Configuration can be set with `MozAdsClientBuilder`. Per-request cache settings can be set with `MozAdsRequestOptions`.
4968
> - If `cache_config` is `None`, caching is disabled entirely.
5069
5170
---
5271

53-
## `MozAdsClientConfig`
72+
## `MozAdsClientBuilder`
5473

55-
Configuration for initializing the ads client.
74+
Builder for configuring and creating the ads client. Use the fluent builder pattern to set configuration options.
5675

5776
```rust
58-
pub struct MozAdsClientConfig {
59-
pub environment: Environment,
60-
pub cache_config: Option<MozAdsCacheConfig>,
61-
pub telemetry: Option<Arc<dyn MozAdsTelemetry>>,
62-
}
77+
pub struct MozAdsClientBuilder
6378
```
6479

65-
| Field | Type | Description |
80+
#### Methods
81+
82+
- **`new() -> MozAdsClientBuilder`** - Creates a new builder with default values
83+
- **`environment(self, environment: MozAdsEnvironment) -> Self`** - Sets the MARS environment (Prod, Staging, or Test)
84+
- **`cache_config(self, cache_config: MozAdsCacheConfig) -> Self`** - Sets the cache configuration
85+
- **`telemetry(self, telemetry: Arc<dyn MozAdsTelemetry>) -> Self`** - Sets the telemetry implementation
86+
- **`build(self) -> MozAdsClient`** - Builds and returns the configured client
87+
88+
| Configuration | Type | Description |
6689
| -------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------ |
67-
| `environment` | `Environment` | Selects which MARS environment to connect to. Unless in a dev build, this value can only ever be Prod. |
90+
| `environment` | `MozAdsEnvironment` | Selects which MARS environment to connect to. Unless in a dev build, this value can only ever be Prod. Defaults to Prod. |
6891
| `cache_config` | `Option<MozAdsCacheConfig>` | Optional configuration for the internal cache. |
6992
| `telemetry` | `Option<Arc<dyn MozAdsTelemetry>>` | Optional telemetry instance for recording metrics. If not provided, a no-op implementation is used. |
7093

7194
---
7295

7396
## `MozAdsTelemetry`
7497

75-
Telemetry interface for recording ads client metrics. You must provide an implementation of this interface to the `MozAdsClientConfig` constructor to enable telemetry collection. If no telemetry instance is provided, a no-op implementation is used and no metrics will be recorded.
98+
Telemetry interface for recording ads client metrics. You must provide an implementation of this interface to the `MozAdsClientBuilder` constructor to enable telemetry collection. If no telemetry instance is provided, a no-op implementation is used and no metrics will be recorded.
7699

77100
```rust
78101
pub trait MozAdsTelemetry: Send + Sync {
@@ -86,7 +109,7 @@ pub trait MozAdsTelemetry: Send + Sync {
86109

87110
### Implementing Telemetry
88111

89-
To enable telemetry collection, you need to implement the `MozAdsTelemetry` interface and provide an instance to the `MozAdsClientConfig` constructor. The following examples show how to bind Glean metrics to the telemetry interface.
112+
To enable telemetry collection, you need to implement the `MozAdsTelemetry` interface and provide an instance to the `MozAdsClientBuilder` constructor. The following examples show how to bind Glean metrics to the telemetry interface.
90113

91114
#### Swift Example
92115

@@ -489,13 +512,12 @@ let cache = MozAdsCacheConfig(
489512
)
490513

491514
let telemetry = AdsClientTelemetry()
492-
let clientCfg = MozAdsClientConfig(
493-
environment: .prod,
494-
cacheConfig: cache,
495-
telemetry: telemetry
496-
)
497515

498-
let client = MozAdsClient(clientConfig: clientCfg)
516+
let client = MozAdsClientBuilder()
517+
.environment(environment: .prod)
518+
.cacheConfig(cacheConfig: cache)
519+
.telemetry(telemetry: telemetry)
520+
.build()
499521
```
500522

501523
```kotlin
@@ -507,13 +529,12 @@ val cache = MozAdsCacheConfig(
507529
)
508530

509531
val telemetry = AdsClientTelemetry()
510-
val clientCfg = MozAdsClientConfig(
511-
environment = MozAdsEnvironment.PROD,
512-
cacheConfig = cache,
513-
telemetry = telemetry
514-
)
515532

516-
val client = MozAdsClient(clientCfg)
533+
val client = MozAdsClientBuilder()
534+
.environment(MozAdsEnvironment.PROD)
535+
.cacheConfig(cache)
536+
.telemetry(telemetry)
537+
.build()
517538
```
518539

519540
Where `db_path` represents the location of the SQLite file. This must be a file that the client has permission to write to.

components/ads-client/src/ffi.rs

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ use crate::client::ad_response::{
1212
AdCallbacks, AdImage, AdSpoc, AdTile, SpocFrequencyCaps, SpocRanking,
1313
};
1414
use crate::client::config::{AdsCacheConfig, AdsClientConfig, Environment};
15+
use crate::client::AdsClient;
1516
use crate::error::ComponentError;
1617
use crate::ffi::telemetry::MozAdsTelemetryWrapper;
1718
use crate::http_cache::{CacheMode, RequestCachePolicy};
19+
use crate::MozAdsClient;
1820
use error_support::{ErrorHandling, GetErrorHandling};
21+
use parking_lot::Mutex;
1922
use url::Url;
2023

2124
pub type AdsClientApiResult<T> = std::result::Result<T, MozAdsClientApiError>;
@@ -88,23 +91,58 @@ pub struct MozAdsCallbacks {
8891
pub report: Option<Url>,
8992
}
9093

91-
#[derive(Default, uniffi::Record)]
92-
pub struct MozAdsClientConfig {
93-
pub environment: MozAdsEnvironment,
94-
pub cache_config: Option<MozAdsCacheConfig>,
95-
pub telemetry: Option<Arc<dyn MozAdsTelemetry>>,
94+
#[derive(uniffi::Object)]
95+
pub struct MozAdsClientBuilder(Mutex<MozAdsClientBuilderInner>);
96+
97+
#[derive(Default)]
98+
struct MozAdsClientBuilderInner {
99+
environment: Option<MozAdsEnvironment>,
100+
cache_config: Option<MozAdsCacheConfig>,
101+
telemetry: Option<Arc<dyn MozAdsTelemetry>>,
96102
}
97103

98-
impl From<MozAdsClientConfig> for AdsClientConfig<MozAdsTelemetryWrapper> {
99-
fn from(config: MozAdsClientConfig) -> Self {
100-
let telemetry = config
101-
.telemetry
102-
.map(MozAdsTelemetryWrapper::new)
103-
.unwrap_or_else(MozAdsTelemetryWrapper::noop);
104-
Self {
105-
environment: config.environment.into(),
106-
cache_config: config.cache_config.map(Into::into),
107-
telemetry,
104+
impl Default for MozAdsClientBuilder {
105+
fn default() -> Self {
106+
Self(Mutex::new(MozAdsClientBuilderInner::default()))
107+
}
108+
}
109+
110+
#[uniffi::export]
111+
impl MozAdsClientBuilder {
112+
#[uniffi::constructor]
113+
pub fn new() -> Self {
114+
Self::default()
115+
}
116+
117+
pub fn environment(self: Arc<Self>, environment: MozAdsEnvironment) -> Arc<Self> {
118+
self.0.lock().environment = Some(environment);
119+
self
120+
}
121+
122+
pub fn cache_config(self: Arc<Self>, cache_config: MozAdsCacheConfig) -> Arc<Self> {
123+
self.0.lock().cache_config = Some(cache_config);
124+
self
125+
}
126+
127+
pub fn telemetry(self: Arc<Self>, telemetry: Arc<dyn MozAdsTelemetry>) -> Arc<Self> {
128+
self.0.lock().telemetry = Some(telemetry);
129+
self
130+
}
131+
132+
pub fn build(&self) -> MozAdsClient {
133+
let inner = self.0.lock();
134+
let client_config = AdsClientConfig {
135+
environment: inner.environment.unwrap_or_default().into(),
136+
cache_config: inner.cache_config.clone().map(Into::into),
137+
telemetry: inner
138+
.telemetry
139+
.clone()
140+
.map(MozAdsTelemetryWrapper::new)
141+
.unwrap_or_else(MozAdsTelemetryWrapper::noop),
142+
};
143+
let client = AdsClient::new(client_config);
144+
MozAdsClient {
145+
inner: Mutex::new(client),
108146
}
109147
}
110148
}
@@ -118,7 +156,7 @@ pub enum MozAdsEnvironment {
118156
Test,
119157
}
120158

121-
#[derive(uniffi::Record)]
159+
#[derive(Clone, uniffi::Record)]
122160
pub struct MozAdsCacheConfig {
123161
pub db_path: String,
124162
pub default_cache_ttl_seconds: Option<u64>,

components/ads-client/src/lib.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ pub mod telemetry;
2323

2424
pub use ffi::*;
2525

26-
use crate::client::config::AdsClientConfig;
2726
use crate::ffi::telemetry::MozAdsTelemetryWrapper;
2827

2928
#[cfg(test)]
@@ -41,18 +40,9 @@ uniffi::custom_type!(AdsClientUrl, String, {
4140
pub struct MozAdsClient {
4241
inner: Mutex<AdsClient<MozAdsTelemetryWrapper>>,
4342
}
43+
4444
#[uniffi::export]
4545
impl MozAdsClient {
46-
#[uniffi::constructor]
47-
pub fn new(client_config: Option<MozAdsClientConfig>) -> Self {
48-
let client_config = client_config.unwrap_or_default();
49-
let client_config: AdsClientConfig<MozAdsTelemetryWrapper> = client_config.into();
50-
let client = AdsClient::new(client_config);
51-
Self {
52-
inner: Mutex::new(client),
53-
}
54-
}
55-
5646
#[handle_error(ComponentError)]
5747
pub fn request_image_ads(
5848
&self,

components/ads-client/tests/integration_test.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::time::Duration;
77

88
use ads_client::{
99
http_cache::{ByteSize, CacheOutcome, HttpCache, RequestCachePolicy},
10-
MozAdsClient, MozAdsPlacementRequest, MozAdsPlacementRequestWithCount,
10+
MozAdsClientBuilder, MozAdsPlacementRequest, MozAdsPlacementRequestWithCount,
1111
};
1212
use url::Url;
1313
use viaduct::Request;
@@ -17,7 +17,7 @@ use viaduct::Request;
1717
fn test_mock_pocket_billboard_1_placement() {
1818
viaduct_dev::init_backend_dev();
1919

20-
let client = MozAdsClient::new(None);
20+
let client = MozAdsClientBuilder::new().build();
2121

2222
let placement_request = MozAdsPlacementRequest {
2323
placement_id: "mock_pocket_billboard_1".to_string(),
@@ -45,7 +45,7 @@ fn test_mock_pocket_billboard_1_placement() {
4545
fn test_newtab_spocs_placement() {
4646
viaduct_dev::init_backend_dev();
4747

48-
let client = MozAdsClient::new(None);
48+
let client = MozAdsClientBuilder::new().build();
4949

5050
let count = 3;
5151
let placement_request = MozAdsPlacementRequestWithCount {
@@ -81,7 +81,7 @@ fn test_newtab_spocs_placement() {
8181
fn test_newtab_tile_1_placement() {
8282
viaduct_dev::init_backend_dev();
8383

84-
let client = MozAdsClient::new(None);
84+
let client = MozAdsClientBuilder::new().build();
8585

8686
let placement_request = MozAdsPlacementRequest {
8787
placement_id: "newtab_tile_1".to_string(),

0 commit comments

Comments
 (0)