use crate::trace_context::{TraceContext, CAUSATION_ID, CORRELATION_ID, TRACEPARENT, TRACESTATE};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize)]
pub enum MessageKind {
Command,
Event,
}
impl MessageKind {
pub fn as_str(&self) -> &'static str {
match self {
MessageKind::Command => "command",
MessageKind::Event => "event",
}
}
pub fn from_str_lossy(value: &str) -> MessageKind {
match value {
"command" => MessageKind::Command,
_ => MessageKind::Event,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SubscriptionPlan {
pub commands: Vec<String>,
pub events: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PayloadDecodeError(pub String);
impl std::fmt::Display for PayloadDecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for PayloadDecodeError {}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Message {
pub id: Option<String>,
pub name: String,
pub kind: MessageKind,
pub payload: Vec<u8>,
pub content_type: String,
pub metadata: Vec<(String, String)>,
}
impl Message {
pub fn new(name: impl Into<String>, kind: MessageKind, payload: Vec<u8>) -> Self {
Self {
id: None,
name: name.into(),
kind,
payload,
content_type: "application/json".to_string(),
metadata: Vec::new(),
}
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.push((key.into(), value.into()));
self
}
pub fn with_trace_context(mut self, context: &TraceContext) -> Self {
context.inject_vec(&mut self.metadata);
self
}
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn validate_name(&self) -> Result<(), super::MessageNameError> {
super::validate_message_name(&self.name).map(|_| ())
}
pub fn payload(&self) -> &[u8] {
&self.payload
}
pub fn metadata(&self, key: &str) -> Option<&str> {
self.metadata
.iter()
.find(|(existing, _)| existing.eq_ignore_ascii_case(key))
.map(|(_, value)| value.as_str())
}
pub fn correlation_id(&self) -> Option<&str> {
self.metadata(CORRELATION_ID)
}
pub fn causation_id(&self) -> Option<&str> {
self.metadata(CAUSATION_ID)
}
pub fn traceparent(&self) -> Option<&str> {
self.metadata(TRACEPARENT)
}
pub fn tracestate(&self) -> Option<&str> {
self.metadata(TRACESTATE)
}
pub fn trace_context(&self) -> TraceContext {
TraceContext::from_metadata(self.metadata.iter().map(|(key, value)| (key, value)))
}
pub fn payload_json<T: serde::de::DeserializeOwned>(&self) -> Result<T, PayloadDecodeError> {
serde_json::from_slice(&self.payload).map_err(|e| {
PayloadDecodeError(format!(
"invalid JSON payload for message '{}': {}",
self.name, e
))
})
}
pub fn payload_bitcode<T: serde::de::DeserializeOwned>(&self) -> Result<T, PayloadDecodeError> {
bitcode::deserialize(&self.payload).map_err(|e| {
PayloadDecodeError(format!(
"invalid bitcode payload for message '{}': {}",
self.name, e
))
})
}
}
#[cfg(any(feature = "nats", feature = "kafka", feature = "rabbitmq"))]
pub(crate) fn message_from_wire(
name: String,
payload: Vec<u8>,
id_key: Option<&str>,
kind_key: &str,
headers: impl IntoIterator<Item = (String, String)>,
) -> Message {
let mut id = None;
let mut kind = MessageKind::Event;
let mut metadata = Vec::new();
for (key, value) in headers {
if id_key == Some(key.as_str()) {
id = Some(value);
} else if key == kind_key {
kind = MessageKind::from_str_lossy(&value);
} else {
metadata.push((key, value));
}
}
let mut message = Message::new(name, kind, payload);
message.id = id;
message.metadata = metadata;
message
}
#[cfg(any(feature = "nats", feature = "kafka", feature = "rabbitmq"))]
pub(crate) fn strip_address_prefix(address: String, prefix: Option<&str>) -> String {
match prefix {
Some(prefix) => address
.strip_prefix(prefix)
.map(str::to_string)
.unwrap_or(address),
None => address,
}
}
#[cfg(test)]
mod tests {
use super::*;
const TRACEPARENT_VALUE: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
#[test]
fn message_trace_context_uses_case_insensitive_metadata() {
let message = Message::new("checkout.started", MessageKind::Event, Vec::new())
.with_metadata("TraceParent", TRACEPARENT_VALUE)
.with_metadata("TRACESTATE", "vendor=value");
assert_eq!(message.traceparent(), Some(TRACEPARENT_VALUE));
assert_eq!(
message.trace_context(),
TraceContext {
traceparent: Some(TRACEPARENT_VALUE.to_string()),
tracestate: Some("vendor=value".to_string()),
}
);
}
#[test]
fn with_trace_context_replaces_duplicate_keys() {
let context = TraceContext {
traceparent: Some(TRACEPARENT_VALUE.to_string()),
tracestate: Some("vendor=value".to_string()),
};
let message = Message::new("checkout.started", MessageKind::Event, Vec::new())
.with_metadata("TraceParent", "old")
.with_trace_context(&context);
assert_eq!(message.traceparent(), Some(TRACEPARENT_VALUE));
assert_eq!(message.tracestate(), Some("vendor=value"));
assert_eq!(
message
.metadata
.iter()
.filter(|(key, _)| key.eq_ignore_ascii_case(TRACEPARENT))
.count(),
1
);
}
}