Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion rust/build.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
use std::env;
use std::path::PathBuf;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let descriptor_path = PathBuf::from(env::var("OUT_DIR")?).join("artf_descriptor.bin");
tonic_prost_build::configure()
.build_server(true)
.build_client(false)
.out_dir("./src")
.file_descriptor_set_path(descriptor_path)
.compile_protos(&["./proto/agenticrtbframeworkservices.proto"], &["proto"])?;
Ok(())
}
}
3 changes: 3 additions & 0 deletions rust/cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ strip = true # Automatically strip symbols from the binary.
opt-level = "z" # Optimize for size.
lto = true # Enable link-time optimization.
codegen-units = 1 # Use a single codegen unit to reduce binary size.

[dev-dependencies]
prost-reflect = { version = "0.16.4", features = ["serde"] }
155 changes: 152 additions & 3 deletions rust/src/bidder/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,33 @@ use crate::mutation::types::{
};
use crate::proto::com::iabtechlab::bidstream::mutation::v1::{RtbRequest, RtbResponse};

/// Evaluate the `RtbRequest` and return an `RtbResponse`.
/// True when `intent` may be returned given the request's applicable intents.
/// An empty list means all intents are applicable, matching the Go
/// implementation's `IsIntentApplicable` semantics.
fn is_intent_applicable(intent: i32, applicable_intents: &[i32]) -> bool {
applicable_intents.is_empty() || applicable_intents.contains(&intent)
}

/// Evaluate the `RtbRequest` and return an `RtbResponse`. Mutations are
/// limited to the intents the request declared applicable.
pub async fn evaluate(req: RtbRequest) -> RtbResponse {
// For demonstration purposes, we will create a static response
// In a real-world scenario, you would implement logic to evaluate the request
// and determine the appropriate mutations based on the request data.
let metadata = build_metadata();

match req.id.as_str() {
// Route on the auction id carried by the enclosed bid request; the
// top-level `req.id` is the extension point envelope id assigned by the
// exchange and is only echoed back in the response.
let auction_id = req
.bid_request
.as_ref()
.and_then(|bid_request| bid_request.id.clone())
.unwrap_or_default();

let applicable_intents = req.applicable_intents.clone();

let mut response = match auction_id.as_str() {
"auction-123" => RtbResponse {
id: req.id,
mutations: vec![
Expand Down Expand Up @@ -49,7 +68,7 @@ pub async fn evaluate(req: RtbRequest) -> RtbResponse {
],
metadata: Some(metadata),
},
"app-123" => RtbResponse {
"auction-native-123" => RtbResponse {
id: req.id,
mutations: vec![
activate_segments(&["demo-18-24"]),
Expand All @@ -62,5 +81,135 @@ pub async fn evaluate(req: RtbRequest) -> RtbResponse {
mutations: vec![],
metadata: Some(metadata),
},
};

response
.mutations
.retain(|mutation| is_intent_applicable(mutation.intent, &applicable_intents));

response
}

#[cfg(test)]
mod tests {
use super::*;
use crate::mutation::types::{PATH_IMP_1, PATH_SEATBID_BID_ABC, PATH_USER_SEGMENT};
use crate::proto::com::iabtechlab::bidstream::mutation::v1::{
mutation::Value, Intent, Operation,
};
use crate::proto::com::iabtechlab::openrtb::v2::BidRequest;

fn request(envelope_id: &str, auction_id: &str) -> RtbRequest {
RtbRequest {
id: envelope_id.to_string(),
bid_request: Some(BidRequest {
id: Some(auction_id.to_string()),
..Default::default()
}),
..Default::default()
}
}

#[tokio::test]
async fn known_auctions_produce_expected_mutation_counts() {
let cases = [
("auction-123", 2),
("auction-456", 2),
("auction-789", 3),
("auction-multi-123", 4),
("auction-native-123", 2),
];
for (auction_id, expected) in cases {
let response = evaluate(request("req-1", auction_id)).await;
assert_eq!(response.mutations.len(), expected, "auction {auction_id}");
assert!(response.metadata.is_some(), "auction {auction_id}");
}
}

#[tokio::test]
async fn response_echoes_envelope_id_not_auction_id() {
let response = evaluate(request("envelope-42", "auction-123")).await;
assert_eq!(response.id, "envelope-42");
}

#[tokio::test]
async fn segment_and_deal_mutations_carry_expected_fields() {
let response = evaluate(request("req-1", "auction-123")).await;

let segments = &response.mutations[0];
assert_eq!(segments.intent, Intent::ActivateSegments as i32);
assert_eq!(segments.op, Operation::Add as i32);
assert_eq!(segments.path, PATH_USER_SEGMENT);
match segments.value.as_ref() {
Some(Value::Ids(ids)) => {
assert_eq!(ids.id, vec!["seg-sports", "demo-25-35", "gender-male"])
}
other => panic!("expected IDs payload, got {other:?}"),
}

let deals = &response.mutations[1];
assert_eq!(deals.intent, Intent::ActivateDeals as i32);
assert_eq!(deals.op, Operation::Add as i32);
assert_eq!(deals.path, PATH_IMP_1);
}

#[tokio::test]
async fn bid_shade_mutation_replaces_price_at_bid_path() {
let response = evaluate(request("req-1", "auction-789")).await;

let shade = response.mutations.last().expect("bid shade mutation");
assert_eq!(shade.intent, Intent::BidShade as i32);
assert_eq!(shade.op, Operation::Replace as i32);
assert_eq!(shade.path, PATH_SEATBID_BID_ABC);
match shade.value.as_ref() {
Some(Value::AdjustBid(adjust)) => assert_eq!(adjust.price, 4.675),
other => panic!("expected AdjustBid payload, got {other:?}"),
}
}

#[tokio::test]
async fn unknown_auction_returns_no_mutations() {
let response = evaluate(request("req-1", "auction-unknown")).await;
assert!(response.mutations.is_empty());
assert!(response.metadata.is_some());
}

#[tokio::test]
async fn mutations_are_limited_to_applicable_intents() {
let mut req = request("req-1", "auction-789");
req.applicable_intents = vec![Intent::BidShade as i32];

let response = evaluate(req).await;

assert_eq!(response.mutations.len(), 1);
assert_eq!(response.mutations[0].intent, Intent::BidShade as i32);
}

#[tokio::test]
async fn empty_applicable_intents_allows_all_mutations() {
let response = evaluate(request("req-1", "auction-789")).await;
assert_eq!(response.mutations.len(), 3);
}

#[tokio::test]
async fn no_matching_applicable_intent_returns_no_mutations() {
let mut req = request("req-1", "auction-123");
req.applicable_intents = vec![Intent::AdjustDealFloor as i32];

let response = evaluate(req).await;

assert!(response.mutations.is_empty());
assert!(response.metadata.is_some());
}

#[tokio::test]
async fn missing_bid_request_returns_no_mutations() {
let request = RtbRequest {
id: "req-1".to_string(),
..Default::default()
};
let response = evaluate(request).await;
assert_eq!(response.id, "req-1");
assert!(response.mutations.is_empty());
}
}
2 changes: 2 additions & 0 deletions rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ mod bidder;
mod config;
mod mutation;
mod proto;
#[cfg(test)]
mod sample_tests;
mod service;

use crate::config::{Config, API_VERSION, MODEL_VERSION};
Expand Down
48 changes: 48 additions & 0 deletions rust/src/mutation/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,51 @@ fn ids_payload(ids: &[&str]) -> IDsPayload {
id: ids.iter().map(|id| id.to_string()).collect(),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn metadata_uses_configured_versions() {
let metadata = build_metadata();
assert_eq!(metadata.api_version, API_VERSION);
assert_eq!(metadata.model_version, MODEL_VERSION);
}

#[test]
fn activate_segments_adds_ids_at_user_segment_path() {
let mutation = activate_segments(&["seg-1", "seg-2"]);
assert_eq!(mutation.intent, Intent::ActivateSegments as i32);
assert_eq!(mutation.op, Operation::Add as i32);
assert_eq!(mutation.path, PATH_USER_SEGMENT);
match mutation.value {
Some(Value::Ids(ids)) => assert_eq!(ids.id, vec!["seg-1", "seg-2"]),
other => panic!("expected IDs payload, got {other:?}"),
}
}

#[test]
fn activate_deals_adds_ids_at_given_path() {
let mutation = activate_deals("/imp/imp-1", &["deal-1"]);
assert_eq!(mutation.intent, Intent::ActivateDeals as i32);
assert_eq!(mutation.op, Operation::Add as i32);
assert_eq!(mutation.path, "/imp/imp-1");
match mutation.value {
Some(Value::Ids(ids)) => assert_eq!(ids.id, vec!["deal-1"]),
other => panic!("expected IDs payload, got {other:?}"),
}
}

#[test]
fn bid_shade_replaces_price_at_given_path() {
let mutation = bid_shade("/seatbid/dsp-001/bid/bid-abc", 4.675);
assert_eq!(mutation.intent, Intent::BidShade as i32);
assert_eq!(mutation.op, Operation::Replace as i32);
assert_eq!(mutation.path, "/seatbid/dsp-001/bid/bid-abc");
match mutation.value {
Some(Value::AdjustBid(adjust)) => assert_eq!(adjust.price, 4.675),
other => panic!("expected AdjustBid payload, got {other:?}"),
}
}
}
Loading