Skip to content

Commit bc6973b

Browse files
Address pr comments
1 parent 436af72 commit bc6973b

File tree

7 files changed

+139
-77
lines changed

7 files changed

+139
-77
lines changed

crates/common/src/auction/context.rs

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,25 @@
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]
3553
pub 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)]
8897
mod 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
}

crates/common/src/auction/formats.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ use fastly::http::{header, StatusCode};
99
use fastly::{Request, Response};
1010
use serde::Deserialize;
1111
use serde_json::Value as JsonValue;
12-
use std::collections::HashMap;
12+
use std::collections::{HashMap, HashSet};
1313
use uuid::Uuid;
1414

15+
use crate::auction::context::ContextValue;
1516
use crate::auction::types::OrchestratorExt;
1617
use crate::creative;
1718
use crate::error::TrustedServerError;
@@ -138,13 +139,28 @@ pub fn convert_tsjs_to_auction_request(
138139
// Only keys listed in `auction.allowed_context_keys` are accepted;
139140
// unrecognised keys are silently dropped to prevent injection of
140141
// arbitrary data by a malicious client payload.
141-
let allowed = &settings.auction.allowed_context_keys;
142+
let allowed: HashSet<&str> = settings
143+
.auction
144+
.allowed_context_keys
145+
.iter()
146+
.map(String::as_str)
147+
.collect();
142148
let mut context = HashMap::new();
143149
if let Some(ref config) = body.config {
144150
if let Some(obj) = config.as_object() {
145151
for (key, value) in obj {
146-
if allowed.iter().any(|k| k == key) {
147-
context.insert(key.clone(), value.clone());
152+
if allowed.contains(key.as_str()) {
153+
match serde_json::from_value::<ContextValue>(value.clone()) {
154+
Ok(cv) => {
155+
context.insert(key.clone(), cv);
156+
}
157+
Err(_) => {
158+
log::debug!(
159+
"Auction context: dropping key '{}' with unsupported type",
160+
key
161+
);
162+
}
163+
}
148164
} else {
149165
log::debug!("Auction context: dropping disallowed key '{}'", key);
150166
}

crates/common/src/auction/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub mod provider;
1919
pub mod types;
2020

2121
pub use config::AuctionConfig;
22-
pub use context::{build_url_with_context_params, ContextQueryParams};
22+
pub use context::{build_url_with_context_params, ContextQueryParams, ContextValue};
2323
pub use orchestrator::AuctionOrchestrator;
2424
pub use provider::AuctionProvider;
2525
pub use types::{

crates/common/src/auction/types.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use fastly::Request;
44
use serde::{Deserialize, Serialize};
55
use std::collections::HashMap;
66

7+
use crate::auction::context::ContextValue;
78
use crate::geo::GeoInfo;
89
use crate::settings::Settings;
910

@@ -22,8 +23,8 @@ pub struct AuctionRequest {
2223
pub device: Option<DeviceInfo>,
2324
/// Site information
2425
pub site: Option<SiteInfo>,
25-
/// Additional context
26-
pub context: HashMap<String, serde_json::Value>,
26+
/// Additional context forwarded from the JS client payload.
27+
pub context: HashMap<String, ContextValue>,
2728
}
2829

2930
/// Represents a single ad slot/impression.

crates/common/src/auction_config_types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ fn default_creative_store() -> String {
5757
}
5858

5959
fn default_allowed_context_keys() -> Vec<String> {
60-
vec!["permutive_segments".to_string()]
60+
vec![]
6161
}
6262

6363
#[allow(dead_code)] // Methods used in runtime but not in build script

0 commit comments

Comments
 (0)