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
73 changes: 68 additions & 5 deletions 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(())
}
}
4 changes: 4 additions & 0 deletions rust/cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ tokio-stream = "0.1.17"
async-stream = "0.3.6"
futures = "0.3.31"
futures-util = "0.3.31"
tonic-reflection = "0.14.6"

[build-dependencies]
tonic-prost-build = "0.14.2"
Expand All @@ -30,3 +31,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"] }
109 changes: 107 additions & 2 deletions rust/src/bidder/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ pub async fn evaluate(req: RtbRequest) -> RtbResponse {
// 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();

match auction_id.as_str() {
"auction-123" => RtbResponse {
id: req.id,
mutations: vec![
Expand Down Expand Up @@ -49,7 +58,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 @@ -64,3 +73,99 @@ pub async fn evaluate(req: RtbRequest) -> RtbResponse {
},
}
}

#[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 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());
}
}
11 changes: 11 additions & 0 deletions rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ mod bidder;
mod config;
mod mutation;
mod proto;
#[cfg(test)]
mod sample_tests;
mod service;

use crate::config::{Config, API_VERSION, MODEL_VERSION};
use crate::proto::com::iabtechlab::bidstream::mutation::services::v1::rtb_extension_point_server::RtbExtensionPointServer;
use crate::service::{handle_rest, RtbExtensionPointService};

/// File descriptor set emitted by `build.rs`, served via gRPC reflection so
/// clients like grpcurl can discover the service without local proto files.
const FILE_DESCRIPTOR_SET: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/artf_descriptor.bin"));

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::from_env();
Expand All @@ -41,11 +47,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
address, config.http_port
);

let reflection_service = tonic_reflection::server::Builder::configure()
.register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
.build_v1()?;

let max_server_connection = config.max_server_connection;
let grpc_server = tokio::spawn(async move {
Server::builder()
.concurrency_limit_per_connection(max_server_connection as usize)
.add_service(agentic_rtb_framework_service)
.add_service(reflection_service)
.serve(grpc_addr)
.await

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