use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct CallContext {
method: String,
caller_identity: Option<String>,
extensions: Vec<String>,
request_id: Option<String>,
http_headers: HashMap<String, String>,
}
impl CallContext {
#[must_use]
pub fn method(&self) -> &str {
&self.method
}
#[must_use]
pub fn caller_identity(&self) -> Option<&str> {
self.caller_identity.as_deref()
}
#[must_use]
pub fn extensions(&self) -> &[String] {
&self.extensions
}
#[must_use]
pub fn request_id(&self) -> Option<&str> {
self.request_id.as_deref()
}
#[must_use]
pub const fn http_headers(&self) -> &HashMap<String, String> {
&self.http_headers
}
}
impl CallContext {
#[must_use]
pub fn new(method: impl Into<String>) -> Self {
Self {
method: method.into(),
caller_identity: None,
extensions: Vec::new(),
request_id: None,
http_headers: HashMap::new(),
}
}
#[must_use]
pub fn with_caller_identity(mut self, identity: String) -> Self {
self.caller_identity = Some(identity);
self
}
#[must_use]
pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
self.extensions = extensions;
self
}
#[must_use]
pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
self.request_id = Some(id.into());
self
}
#[must_use]
pub fn with_http_headers(mut self, headers: HashMap<String, String>) -> Self {
if let Some(rid) = headers.get("x-request-id") {
self.request_id = Some(rid.clone());
}
self.http_headers = headers;
self
}
#[must_use]
pub fn with_http_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
let key = key.into().to_ascii_lowercase();
let value = value.into();
if key == "x-request-id" {
self.request_id = Some(value.clone());
}
self.http_headers.insert(key, value);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn with_http_header_x_request_id_populates_request_id() {
let ctx = CallContext::new("test").with_http_header("x-request-id", "req-42");
assert_eq!(ctx.request_id(), Some("req-42"));
assert_eq!(
ctx.http_headers().get("x-request-id").map(String::as_str),
Some("req-42")
);
}
#[test]
fn with_http_header_other_key_does_not_populate_request_id() {
let ctx = CallContext::new("test").with_http_header("authorization", "Bearer tok");
assert!(ctx.request_id().is_none());
assert_eq!(
ctx.http_headers().get("authorization").map(String::as_str),
Some("Bearer tok")
);
}
#[test]
fn with_request_id_sets_field() {
let ctx = CallContext::new("test").with_request_id("req-99");
assert_eq!(ctx.request_id(), Some("req-99"));
}
#[test]
fn with_http_headers_extracts_request_id() {
let mut headers = HashMap::new();
headers.insert("x-request-id".to_owned(), "trace-123".to_owned());
headers.insert("content-type".to_owned(), "application/json".to_owned());
let ctx = CallContext::new("test").with_http_headers(headers);
assert_eq!(ctx.request_id(), Some("trace-123"));
assert_eq!(
ctx.http_headers().get("content-type").map(String::as_str),
Some("application/json")
);
}
#[test]
fn with_http_headers_without_request_id() {
let mut headers = HashMap::new();
headers.insert("authorization".to_owned(), "Bearer tok".to_owned());
let ctx = CallContext::new("test").with_http_headers(headers);
assert!(ctx.request_id().is_none());
}
#[test]
fn with_caller_identity_sets_field() {
let ctx = CallContext::new("test").with_caller_identity("user@example.com".into());
assert_eq!(ctx.caller_identity(), Some("user@example.com"));
}
#[test]
fn with_extensions_sets_field() {
let ctx = CallContext::new("test").with_extensions(vec!["ext1".into(), "ext2".into()]);
assert_eq!(ctx.extensions(), &["ext1", "ext2"]);
}
#[test]
fn new_defaults_are_empty() {
let ctx = CallContext::new("method");
assert_eq!(ctx.method(), "method");
assert!(ctx.caller_identity().is_none());
assert!(ctx.extensions().is_empty());
assert!(ctx.request_id().is_none());
assert!(ctx.http_headers().is_empty());
}
}