use axum::http::HeaderMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
pub struct RequestContext {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub operation_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub work_item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub actor_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grant_id: Option<String>,
}
#[allow(dead_code)]
impl RequestContext {
pub fn builder() -> RequestContextBuilder {
RequestContextBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
pub struct RequestContextBuilder {
operation_id: Option<String>,
work_item_id: Option<String>,
actor_id: Option<String>,
grant_id: Option<String>,
}
#[allow(dead_code)]
impl RequestContextBuilder {
pub fn operation_id(mut self, id: impl Into<String>) -> Self {
self.operation_id = Some(id.into());
self
}
pub fn work_item_id(mut self, id: impl Into<String>) -> Self {
self.work_item_id = Some(id.into());
self
}
pub fn actor_id(mut self, id: impl Into<String>) -> Self {
self.actor_id = Some(id.into());
self
}
pub fn grant_id(mut self, id: impl Into<String>) -> Self {
self.grant_id = Some(id.into());
self
}
pub fn build(self) -> RequestContext {
RequestContext {
operation_id: self.operation_id,
work_item_id: self.work_item_id,
actor_id: self.actor_id,
grant_id: self.grant_id,
}
}
}
pub fn extract_header_str(headers: &HeaderMap, key: impl AsRef<str>) -> Option<String> {
headers
.get(key.as_ref())
.and_then(|v| v.to_str().ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
pub fn resolve_request_id(
payload_id: Option<String>,
headers: &HeaderMap,
fallback_trace_id: impl Into<String>,
) -> String {
if let Some(id) = payload_id.filter(|s| !s.trim().is_empty()) {
return id;
}
if let Some(id) = extract_header_str(headers, "x-request-id")
.or_else(|| extract_header_str(headers, "x-correlation-id"))
{
return id;
}
fallback_trace_id.into()
}
pub fn resolve_request_context(
payload_context: Option<RequestContext>,
headers: &HeaderMap,
) -> RequestContext {
let base = payload_context.unwrap_or_default();
RequestContext {
operation_id: base
.operation_id
.or_else(|| extract_header_str(headers, "x-operation-id")),
work_item_id: base
.work_item_id
.or_else(|| extract_header_str(headers, "x-work-item-id")),
actor_id: base
.actor_id
.or_else(|| extract_header_str(headers, "x-actor-id")),
grant_id: base
.grant_id
.or_else(|| extract_header_str(headers, "x-grant-id")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_resolution_combines_payload_and_headers() {
let mut headers = HeaderMap::new();
headers.insert("x-request-id", "hdr-req-123".parse().unwrap());
headers.insert("x-actor-id", "hdr-actor-9".parse().unwrap());
let payload_ctx = RequestContext {
operation_id: Some("payload-op-1".to_string()),
..Default::default()
};
let req_id = resolve_request_id(None, &headers, "fallback-1".to_string());
assert_eq!(req_id, "hdr-req-123");
let resolved_ctx = resolve_request_context(Some(payload_ctx), &headers);
assert_eq!(resolved_ctx.operation_id.as_deref(), Some("payload-op-1"));
assert_eq!(resolved_ctx.actor_id.as_deref(), Some("hdr-actor-9"));
assert_eq!(resolved_ctx.work_item_id, None);
}
}