44//! forward integration-supplied data (e.g. audience segments) as URL query
55//! parameters without hard-coding integration-specific knowledge.
66
7- use std:: collections:: HashMap ;
7+ use serde:: { Deserialize , Serialize } ;
8+ use std:: collections:: { BTreeMap , HashMap } ;
9+
10+ /// A strongly-typed context value forwarded from the JS client payload.
11+ ///
12+ /// Replaces raw `serde_json::Value` so that consumers get compile-time
13+ /// exhaustiveness checks. The `#[serde(untagged)]` attribute preserves
14+ /// wire-format compatibility — the JS client sends plain JSON arrays, strings,
15+ /// or numbers which serde maps to the matching variant in declaration order.
16+ #[ derive( Debug , Clone , Serialize , Deserialize , PartialEq ) ]
17+ #[ serde( untagged) ]
18+ pub enum ContextValue {
19+ /// A list of string values (e.g. audience segment IDs).
20+ StringList ( Vec < String > ) ,
21+ /// A single string value.
22+ Text ( String ) ,
23+ /// A numeric value.
24+ Number ( f64 ) ,
25+ }
826
927/// Mapping from auction-request context keys to query-parameter names.
1028///
@@ -17,7 +35,7 @@ use std::collections::HashMap;
1735/// permutive_segments = "permutive"
1836/// lockr_ids = "lockr"
1937/// ```
20- pub type ContextQueryParams = HashMap < String , String > ;
38+ pub type ContextQueryParams = BTreeMap < String , String > ;
2139
2240/// Build a URL by appending context values as query parameters according to the
2341/// provided mapping.
@@ -34,7 +52,7 @@ pub type ContextQueryParams = HashMap<String, String>;
3452#[ must_use]
3553pub fn build_url_with_context_params (
3654 base_url : & str ,
37- context : & HashMap < String , serde_json :: Value > ,
55+ context : & HashMap < String , ContextValue > ,
3856 mapping : & ContextQueryParams ,
3957) -> String {
4058 let Ok ( mut url) = url:: Url :: parse ( base_url) else {
@@ -64,38 +82,28 @@ pub fn build_url_with_context_params(
6482 url. to_string ( )
6583}
6684
67- /// Serialise a single [`serde_json::Value `] into a string suitable for a query
68- /// parameter value. Arrays are joined with commas; strings and numbers are
69- /// returned directly; anything else yields an empty string (skipped) .
70- fn serialize_context_value ( value : & serde_json :: Value ) -> String {
85+ /// Serialise a single [`ContextValue `] into a string suitable for a query
86+ /// parameter value. String lists are joined with commas; strings and numbers
87+ /// are returned directly.
88+ fn serialize_context_value ( value : & ContextValue ) -> String {
7189 match value {
72- serde_json:: Value :: Array ( arr) => arr
73- . iter ( )
74- . filter_map ( |v| match v {
75- serde_json:: Value :: String ( s) => Some ( s. clone ( ) ) ,
76- serde_json:: Value :: Number ( n) => Some ( n. to_string ( ) ) ,
77- _ => None ,
78- } )
79- . collect :: < Vec < _ > > ( )
80- . join ( "," ) ,
81- serde_json:: Value :: String ( s) => s. clone ( ) ,
82- serde_json:: Value :: Number ( n) => n. to_string ( ) ,
83- _ => String :: new ( ) ,
90+ ContextValue :: StringList ( items) => items. join ( "," ) ,
91+ ContextValue :: Text ( s) => s. clone ( ) ,
92+ ContextValue :: Number ( n) => n. to_string ( ) ,
8493 }
8594}
8695
8796#[ cfg( test) ]
8897mod tests {
8998 use super :: * ;
90- use serde_json:: json;
9199
92100 #[ test]
93101 fn test_build_url_with_context_params_appends_array ( ) {
94102 let context = HashMap :: from ( [ (
95103 "permutive_segments" . to_string ( ) ,
96- json ! ( [ "10000001" , "10000003" , "adv" ] ) ,
104+ ContextValue :: StringList ( vec ! [ "10000001" . into ( ) , "10000003" . into ( ) , "adv" . into ( ) ] ) ,
97105 ) ] ) ;
98- let mapping = HashMap :: from ( [ ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ] ) ;
106+ let mapping = BTreeMap :: from ( [ ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ] ) ;
99107
100108 let url = build_url_with_context_params (
101109 "http://localhost:6767/adserver/mediate" ,
@@ -110,8 +118,11 @@ mod tests {
110118
111119 #[ test]
112120 fn test_build_url_with_context_params_preserves_existing_query ( ) {
113- let context = HashMap :: from ( [ ( "permutive_segments" . to_string ( ) , json ! ( [ "123" , "adv" ] ) ) ] ) ;
114- let mapping = HashMap :: from ( [ ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ] ) ;
121+ let context = HashMap :: from ( [ (
122+ "permutive_segments" . to_string ( ) ,
123+ ContextValue :: StringList ( vec ! [ "123" . into( ) , "adv" . into( ) ] ) ,
124+ ) ] ) ;
125+ let mapping = BTreeMap :: from ( [ ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ] ) ;
115126
116127 let url = build_url_with_context_params (
117128 "http://localhost:6767/adserver/mediate?debug=true" ,
@@ -127,7 +138,7 @@ mod tests {
127138 #[ test]
128139 fn test_build_url_with_context_params_no_matching_keys ( ) {
129140 let context = HashMap :: new ( ) ;
130- let mapping = HashMap :: from ( [ ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ] ) ;
141+ let mapping = BTreeMap :: from ( [ ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ] ) ;
131142
132143 let url = build_url_with_context_params (
133144 "http://localhost:6767/adserver/mediate" ,
@@ -139,8 +150,11 @@ mod tests {
139150
140151 #[ test]
141152 fn test_build_url_with_context_params_empty_array_skipped ( ) {
142- let context = HashMap :: from ( [ ( "permutive_segments" . to_string ( ) , json ! ( [ ] ) ) ] ) ;
143- let mapping = HashMap :: from ( [ ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ] ) ;
153+ let context = HashMap :: from ( [ (
154+ "permutive_segments" . to_string ( ) ,
155+ ContextValue :: StringList ( vec ! [ ] ) ,
156+ ) ] ) ;
157+ let mapping = BTreeMap :: from ( [ ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ] ) ;
144158
145159 let url = build_url_with_context_params (
146160 "http://localhost:6767/adserver/mediate" ,
@@ -153,12 +167,18 @@ mod tests {
153167 #[ test]
154168 fn test_build_url_with_context_params_multiple_mappings ( ) {
155169 let context = HashMap :: from ( [
156- ( "permutive_segments" . to_string ( ) , json ! ( [ "seg1" ] ) ) ,
157- ( "lockr_ids" . to_string ( ) , json ! ( "lockr-abc-123" ) ) ,
170+ (
171+ "permutive_segments" . to_string ( ) ,
172+ ContextValue :: StringList ( vec ! [ "seg1" . into( ) ] ) ,
173+ ) ,
174+ (
175+ "lockr_ids" . to_string ( ) ,
176+ ContextValue :: Text ( "lockr-abc-123" . into ( ) ) ,
177+ ) ,
158178 ] ) ;
159- let mapping = HashMap :: from ( [
160- ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ,
179+ let mapping = BTreeMap :: from ( [
161180 ( "lockr_ids" . to_string ( ) , "lockr" . to_string ( ) ) ,
181+ ( "permutive_segments" . to_string ( ) , "permutive" . to_string ( ) ) ,
162182 ] ) ;
163183
164184 let url = build_url_with_context_params (
@@ -172,8 +192,8 @@ mod tests {
172192
173193 #[ test]
174194 fn test_build_url_with_context_params_scalar_number ( ) {
175- let context = HashMap :: from ( [ ( "count" . to_string ( ) , json ! ( 42 ) ) ] ) ;
176- let mapping = HashMap :: from ( [ ( "count" . to_string ( ) , "n" . to_string ( ) ) ] ) ;
195+ let context = HashMap :: from ( [ ( "count" . to_string ( ) , ContextValue :: Number ( 42.0 ) ) ] ) ;
196+ let mapping = BTreeMap :: from ( [ ( "count" . to_string ( ) , "n" . to_string ( ) ) ] ) ;
177197
178198 let url = build_url_with_context_params (
179199 "http://localhost:6767/adserver/mediate" ,
@@ -184,22 +204,45 @@ mod tests {
184204 }
185205
186206 #[ test]
187- fn test_serialize_context_value_array ( ) {
188- assert_eq ! ( serialize_context_value( & json!( [ "a" , "b" , 3 ] ) ) , "a,b,3" ) ;
207+ fn test_serialize_context_value_string_list ( ) {
208+ assert_eq ! (
209+ serialize_context_value( & ContextValue :: StringList ( vec![
210+ "a" . into( ) ,
211+ "b" . into( ) ,
212+ "3" . into( )
213+ ] ) ) ,
214+ "a,b,3"
215+ ) ;
189216 }
190217
191218 #[ test]
192- fn test_serialize_context_value_string ( ) {
193- assert_eq ! ( serialize_context_value( & json!( "hello" ) ) , "hello" ) ;
219+ fn test_serialize_context_value_text ( ) {
220+ assert_eq ! (
221+ serialize_context_value( & ContextValue :: Text ( "hello" . into( ) ) ) ,
222+ "hello"
223+ ) ;
194224 }
195225
196226 #[ test]
197227 fn test_serialize_context_value_number ( ) {
198- assert_eq ! ( serialize_context_value( & json!( 99 ) ) , "99" ) ;
228+ assert_eq ! ( serialize_context_value( & ContextValue :: Number ( 99.0 ) ) , "99" ) ;
229+ }
230+
231+ #[ test]
232+ fn test_context_value_deserialize_array ( ) {
233+ let v: ContextValue = serde_json:: from_str ( r#"["a","b"]"# ) . unwrap ( ) ;
234+ assert_eq ! ( v, ContextValue :: StringList ( vec![ "a" . into( ) , "b" . into( ) ] ) ) ;
235+ }
236+
237+ #[ test]
238+ fn test_context_value_deserialize_string ( ) {
239+ let v: ContextValue = serde_json:: from_str ( r#""hello""# ) . unwrap ( ) ;
240+ assert_eq ! ( v, ContextValue :: Text ( "hello" . into( ) ) ) ;
199241 }
200242
201243 #[ test]
202- fn test_serialize_context_value_object_returns_empty ( ) {
203- assert_eq ! ( serialize_context_value( & json!( { "a" : 1 } ) ) , "" ) ;
244+ fn test_context_value_deserialize_number ( ) {
245+ let v: ContextValue = serde_json:: from_str ( "42" ) . unwrap ( ) ;
246+ assert_eq ! ( v, ContextValue :: Number ( 42.0 ) ) ;
204247 }
205248}
0 commit comments