66use std:: collections:: HashMap ;
77use std:: time:: Duration ;
88
9- use crate :: client:: ad_response:: Ad ;
9+ use crate :: client:: ad_response:: { AdImage , AdResponse , AdSpoc , AdTile } ;
1010use crate :: client:: config:: AdsClientConfig ;
1111use crate :: error:: { RecordClickError , RecordImpressionError , ReportAdError , RequestAdsError } ;
1212use crate :: http_cache:: { HttpCache , RequestCachePolicy } ;
13- use crate :: mars:: { DefaultMARSClient , MARSClient } ;
13+ use crate :: mars:: MARSClient ;
1414use ad_request:: { AdPlacementRequest , AdRequest } ;
15+ use context_id:: { ContextIDComponent , DefaultContextIdCallback } ;
16+ use serde:: de:: DeserializeOwned ;
1517use url:: Url ;
1618use uuid:: Uuid ;
1719
@@ -25,7 +27,8 @@ const DEFAULT_TTL_SECONDS: u64 = 300;
2527const DEFAULT_MAX_CACHE_SIZE_MIB : u64 = 10 ;
2628
2729pub struct AdsClient {
28- client : Box < dyn MARSClient > ,
30+ client : MARSClient ,
31+ context_id_component : ContextIDComponent ,
2932}
3033
3134impl AdsClient {
@@ -34,6 +37,13 @@ impl AdsClient {
3437
3538 let client_config = client_config. unwrap_or_default ( ) ;
3639
40+ let context_id_component = ContextIDComponent :: new (
41+ & context_id,
42+ 0 ,
43+ cfg ! ( test) ,
44+ Box :: new ( DefaultContextIdCallback ) ,
45+ ) ;
46+
3747 // Configure the cache if a path is provided.
3848 // Defaults for ttl and cache size are also set if unspecified.
3949 if let Some ( cache_cfg) = client_config. cache_config {
@@ -51,32 +61,60 @@ impl AdsClient {
5161 . build ( )
5262 . ok ( ) ; // TODO: handle error with telemetry
5363
54- let client = Box :: new ( DefaultMARSClient :: new (
55- context_id,
56- client_config. environment ,
57- http_cache,
58- ) ) ;
59- return Self { client } ;
64+ let client = MARSClient :: new ( client_config. environment , http_cache) ;
65+ return Self {
66+ context_id_component,
67+ client,
68+ } ;
6069 }
6170
62- let client = Box :: new ( DefaultMARSClient :: new (
63- context_id,
64- client_config. environment ,
65- None ,
66- ) ) ;
67- Self { client }
71+ let client = MARSClient :: new ( client_config. environment , None ) ;
72+ Self {
73+ context_id_component,
74+ client,
75+ }
6876 }
6977
70- pub fn request_ads (
78+ fn request_ads < T > (
7179 & self ,
7280 ad_placement_requests : Vec < AdPlacementRequest > ,
7381 options : Option < RequestCachePolicy > ,
74- ) -> Result < HashMap < String , Vec < Ad > > , RequestAdsError > {
75- let ad_request = AdRequest :: build ( self . client . get_context_id ( ) ?, ad_placement_requests) ?;
82+ ) -> Result < AdResponse < T > , RequestAdsError >
83+ where
84+ T : DeserializeOwned ,
85+ {
86+ let context_id = self . get_context_id ( ) ?;
87+ let ad_request = AdRequest :: build ( context_id, ad_placement_requests) ?;
7688 let cache_policy = options. unwrap_or_default ( ) ;
7789 let response = self . client . fetch_ads ( & ad_request, & cache_policy) ?;
78- let placements = response. build_placements ( & ad_request) ?;
79- Ok ( placements)
90+ Ok ( response)
91+ }
92+
93+ pub fn request_image_ads (
94+ & self ,
95+ ad_placement_requests : Vec < AdPlacementRequest > ,
96+ options : Option < RequestCachePolicy > ,
97+ ) -> Result < HashMap < String , AdImage > , RequestAdsError > {
98+ let response = self . request_ads :: < AdImage > ( ad_placement_requests, options) ?;
99+ Ok ( response. take_first ( ) )
100+ }
101+
102+ pub fn request_spoc_ads (
103+ & self ,
104+ ad_placement_requests : Vec < AdPlacementRequest > ,
105+ options : Option < RequestCachePolicy > ,
106+ ) -> Result < HashMap < String , Vec < AdSpoc > > , RequestAdsError > {
107+ let response = self . request_ads :: < AdSpoc > ( ad_placement_requests, options) ?;
108+ Ok ( response. data )
109+ }
110+
111+ pub fn request_tile_ads (
112+ & self ,
113+ ad_placement_requests : Vec < AdPlacementRequest > ,
114+ options : Option < RequestCachePolicy > ,
115+ ) -> Result < HashMap < String , AdTile > , RequestAdsError > {
116+ let response = self . request_ads :: < AdTile > ( ad_placement_requests, options) ?;
117+ Ok ( response. take_first ( ) )
80118 }
81119
82120 pub fn record_impression ( & self , impression_url : Url ) -> Result < ( ) , RecordImpressionError > {
@@ -92,8 +130,14 @@ impl AdsClient {
92130 Ok ( ( ) )
93131 }
94132
133+ pub fn get_context_id ( & self ) -> context_id:: ApiResult < String > {
134+ self . context_id_component . request ( 0 )
135+ }
136+
95137 pub fn cycle_context_id ( & mut self ) -> context_id:: ApiResult < String > {
96- self . client . cycle_context_id ( )
138+ let old_context_id = self . get_context_id ( ) ?;
139+ self . context_id_component . force_rotation ( ) ?;
140+ Ok ( old_context_id)
97141 }
98142
99143 pub fn clear_cache ( & self ) -> Result < ( ) , HttpCacheError > {
@@ -103,78 +147,123 @@ impl AdsClient {
103147
104148#[ cfg( test) ]
105149mod tests {
106- use url:: Url ;
107-
108- use crate :: {
109- client:: ad_request:: { AdContentCategory , IABContentTaxonomy } ,
110- mars:: MockMARSClient ,
111- test_utils:: {
112- get_example_happy_ad_response, get_example_happy_placements,
113- make_happy_placement_requests,
114- } ,
150+ use crate :: test_utils:: {
151+ get_example_happy_image_response, get_example_happy_spoc_response,
152+ get_example_happy_uatile_response, make_happy_placement_requests,
115153 } ;
116154
117155 use super :: * ;
118156
119157 #[ test]
120- fn test_request_ads_happy ( ) {
121- let mut mock = MockMARSClient :: new ( ) ;
122- mock. expect_fetch_ads ( )
123- . returning ( |_req, _| Ok ( get_example_happy_ad_response ( ) ) ) ;
124- mock. expect_get_context_id ( )
125- . returning ( || Ok ( "mock-context-id" . to_string ( ) ) ) ;
158+ fn test_get_context_id ( ) {
159+ let client = AdsClient :: new ( None ) ;
160+ let context_id = client. get_context_id ( ) . unwrap ( ) ;
161+ assert ! ( !context_id. is_empty( ) ) ;
162+ }
126163
127- mock. expect_get_mars_endpoint ( )
128- . return_const ( Url :: parse ( "https://mock.endpoint/ads" ) . unwrap ( ) ) ;
164+ #[ test]
165+ fn test_cycle_context_id ( ) {
166+ let mut client = AdsClient :: new ( None ) ;
167+ let old_id = client. get_context_id ( ) . unwrap ( ) ;
168+ let previous_id = client. cycle_context_id ( ) . unwrap ( ) ;
169+ assert_eq ! ( previous_id, old_id) ;
170+ let new_id = client. get_context_id ( ) . unwrap ( ) ;
171+ assert_ne ! ( new_id, old_id) ;
172+ }
129173
174+ #[ test]
175+ fn test_request_image_ads_happy ( ) {
176+ use crate :: test_utils:: create_test_client;
177+ use context_id:: { ContextIDComponent , DefaultContextIdCallback } ;
178+ viaduct_dev:: init_backend_dev ( ) ;
179+
180+ let expected_response = get_example_happy_image_response ( ) ;
181+ let _m = mockito:: mock ( "POST" , "/ads" )
182+ . with_status ( 200 )
183+ . with_header ( "content-type" , "application/json" )
184+ . with_body ( serde_json:: to_string ( & expected_response) . unwrap ( ) )
185+ . create ( ) ;
186+
187+ let mars_client = create_test_client ( mockito:: server_url ( ) ) ;
188+ let context_id_component = ContextIDComponent :: new (
189+ & uuid:: Uuid :: new_v4 ( ) . to_string ( ) ,
190+ 0 ,
191+ false ,
192+ Box :: new ( DefaultContextIdCallback ) ,
193+ ) ;
130194 let component = AdsClient {
131- client : Box :: new ( mock) ,
195+ context_id_component,
196+ client : mars_client,
132197 } ;
133198
134199 let ad_placement_requests = make_happy_placement_requests ( ) ;
135200
136- let result = component. request_ads ( ad_placement_requests, None ) ;
201+ let result = component. request_image_ads ( ad_placement_requests, None ) ;
137202
138203 assert ! ( result. is_ok( ) ) ;
139204 }
140205
141206 #[ test]
142- fn test_request_ads_multiset_happy ( ) {
143- let mut mock = MockMARSClient :: new ( ) ;
144- mock. expect_fetch_ads ( )
145- . returning ( |_req, _| Ok ( get_example_happy_ad_response ( ) ) ) ;
146- mock. expect_get_context_id ( )
147- . returning ( || Ok ( "mock-context-id" . to_string ( ) ) ) ;
207+ fn test_request_spocs_happy ( ) {
208+ use crate :: test_utils:: create_test_client;
209+ use context_id:: { ContextIDComponent , DefaultContextIdCallback } ;
210+ viaduct_dev:: init_backend_dev ( ) ;
211+
212+ let expected_response = get_example_happy_spoc_response ( ) ;
213+ let _m = mockito:: mock ( "POST" , "/ads" )
214+ . with_status ( 200 )
215+ . with_header ( "content-type" , "application/json" )
216+ . with_body ( serde_json:: to_string ( & expected_response) . unwrap ( ) )
217+ . create ( ) ;
218+
219+ let mars_client = create_test_client ( mockito:: server_url ( ) ) ;
220+ let context_id_component = ContextIDComponent :: new (
221+ & uuid:: Uuid :: new_v4 ( ) . to_string ( ) ,
222+ 0 ,
223+ false ,
224+ Box :: new ( DefaultContextIdCallback ) ,
225+ ) ;
226+ let component = AdsClient {
227+ context_id_component,
228+ client : mars_client,
229+ } ;
148230
149- mock. expect_get_mars_endpoint ( )
150- . return_const ( Url :: parse ( "https://mock.endpoint/ads" ) . unwrap ( ) ) ;
231+ let ad_placement_requests = make_happy_placement_requests ( ) ;
232+
233+ let result = component. request_spoc_ads ( ad_placement_requests, None ) ;
234+
235+ assert ! ( result. is_ok( ) ) ;
236+ }
151237
238+ #[ test]
239+ fn test_request_tiles_happy ( ) {
240+ use crate :: test_utils:: create_test_client;
241+ use context_id:: { ContextIDComponent , DefaultContextIdCallback } ;
242+ viaduct_dev:: init_backend_dev ( ) ;
243+
244+ let expected_response = get_example_happy_uatile_response ( ) ;
245+ let _m = mockito:: mock ( "POST" , "/ads" )
246+ . with_status ( 200 )
247+ . with_header ( "content-type" , "application/json" )
248+ . with_body ( serde_json:: to_string ( & expected_response) . unwrap ( ) )
249+ . create ( ) ;
250+
251+ let mars_client = create_test_client ( mockito:: server_url ( ) ) ;
252+ let context_id_component = ContextIDComponent :: new (
253+ & uuid:: Uuid :: new_v4 ( ) . to_string ( ) ,
254+ 0 ,
255+ false ,
256+ Box :: new ( DefaultContextIdCallback ) ,
257+ ) ;
152258 let component = AdsClient {
153- client : Box :: new ( mock) ,
259+ context_id_component,
260+ client : mars_client,
154261 } ;
155262
156- let ad_placement_requests: Vec < AdPlacementRequest > = vec ! [
157- AdPlacementRequest {
158- placement: "example_placement_1" . to_string( ) ,
159- count: 1 ,
160- content: Some ( AdContentCategory {
161- taxonomy: IABContentTaxonomy :: IAB2_1 ,
162- categories: vec![ "entertainment" . to_string( ) ] ,
163- } ) ,
164- } ,
165- AdPlacementRequest {
166- placement: "example_placement_2" . to_string( ) ,
167- count: 2 ,
168- content: Some ( AdContentCategory {
169- taxonomy: IABContentTaxonomy :: IAB3_0 ,
170- categories: vec![ ] ,
171- } ) ,
172- } ,
173- ] ;
174-
175- let result = component. request_ads ( ad_placement_requests, None ) ;
263+ let ad_placement_requests = make_happy_placement_requests ( ) ;
264+
265+ let result = component. request_tile_ads ( ad_placement_requests, None ) ;
176266
177267 assert ! ( result. is_ok( ) ) ;
178- assert_eq ! ( result. unwrap( ) , get_example_happy_placements( ) ) ;
179268 }
180269}
0 commit comments