pub const CLOUDEVENTS_SPEC_VERSION: &str = "1.0";
#[derive(Clone, Debug)]
pub struct CloudEventsEnvelope<T> {
pub id: std::string::String,
pub source: std::string::String,
pub specversion: std::string::String,
pub r#type: std::string::String,
pub datacontenttype: std::option::Option<std::string::String>,
pub dataschema: std::option::Option<std::string::String>,
pub subject: std::option::Option<std::string::String>,
pub time: std::option::Option<std::string::String>,
pub data: std::option::Option<T>,
pub extensions: std::collections::HashMap<std::string::String, std::string::String>,
}
impl<T> CloudEventsEnvelope<T> {
pub fn new(
id: std::string::String,
source: std::string::String,
event_type: std::string::String,
) -> Self {
Self {
id,
source,
specversion: std::string::String::from(CLOUDEVENTS_SPEC_VERSION),
r#type: event_type,
datacontenttype: std::option::Option::None,
dataschema: std::option::Option::None,
subject: std::option::Option::None,
time: std::option::Option::None,
data: std::option::Option::None,
extensions: std::collections::HashMap::new(),
}
}
pub fn from_domain_event(id: std::string::String, source: std::string::String, event: T) -> Self
where
T: crate::domain::DomainEvent,
{
let event_type = event.event_type().to_string();
let subject = event.aggregate_id();
Self {
id,
source,
specversion: std::string::String::from(CLOUDEVENTS_SPEC_VERSION),
r#type: event_type,
datacontenttype: std::option::Option::Some(std::string::String::from("application/json")),
dataschema: std::option::Option::None,
subject: std::option::Option::Some(subject),
time: std::option::Option::None,
data: std::option::Option::Some(event),
extensions: std::collections::HashMap::new(),
}
}
pub fn validate(&self) -> crate::HexResult<()> {
if self.id.is_empty() {
return std::result::Result::Err(crate::Hexserror::validation(
"CloudEvents id attribute must not be empty",
));
}
if self.source.is_empty() {
return std::result::Result::Err(crate::Hexserror::validation(
"CloudEvents source attribute must not be empty",
));
}
if self.specversion != CLOUDEVENTS_SPEC_VERSION {
return std::result::Result::Err(crate::Hexserror::validation(&format!(
"CloudEvents specversion must be '{}', got '{}'",
CLOUDEVENTS_SPEC_VERSION, self.specversion
)));
}
if self.r#type.is_empty() {
return std::result::Result::Err(crate::Hexserror::validation(
"CloudEvents type attribute must not be empty",
));
}
std::result::Result::Ok(())
}
pub fn validate_time_format(&self) -> crate::HexResult<()> {
if let std::option::Option::Some(ref time_str) = self.time {
if time_str.len() < 20 {
return std::result::Result::Err(crate::Hexserror::validation(
"CloudEvents time attribute must be in RFC3339 format (e.g., 2025-10-09T14:51:00Z)",
));
}
if !time_str.contains('T') {
return std::result::Result::Err(crate::Hexserror::validation(
"CloudEvents time attribute must contain 'T' separator (RFC3339 format)",
));
}
}
std::result::Result::Ok(())
}
pub fn add_extension(
&mut self,
key: std::string::String,
value: std::string::String,
) -> crate::HexResult<()> {
if key.starts_with("ce-") {
return std::result::Result::Err(crate::Hexserror::validation(
"Extension attribute keys must not start with 'ce-' (reserved by CloudEvents specification)",
));
}
self.extensions.insert(key, value);
std::result::Result::Ok(())
}
pub fn get_extension(&self, key: &str) -> std::option::Option<&std::string::String> {
self.extensions.get(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestEvent {
id: std::string::String,
data: std::string::String,
}
impl crate::domain::DomainEvent for TestEvent {
fn event_type(&self) -> &str {
"com.test.event.created"
}
fn aggregate_id(&self) -> std::string::String {
self.id.clone()
}
}
#[test]
fn test_new_envelope_with_required_attributes() {
let envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from("/test/source"),
std::string::String::from("com.test.event"),
);
std::assert_eq!(envelope.id, "evt-001");
std::assert_eq!(envelope.source, "/test/source");
std::assert_eq!(envelope.specversion, "1.0");
std::assert_eq!(envelope.r#type, "com.test.event");
std::assert!(envelope.data.is_none());
std::assert!(envelope.subject.is_none());
}
#[test]
fn test_from_domain_event_maps_attributes() {
let event = TestEvent {
id: std::string::String::from("test-123"),
data: std::string::String::from("test data"),
};
let envelope = CloudEventsEnvelope::from_domain_event(
std::string::String::from("evt-002"),
std::string::String::from("/test/source"),
event,
);
std::assert_eq!(envelope.id, "evt-002");
std::assert_eq!(envelope.r#type, "com.test.event.created");
std::assert_eq!(
envelope.subject,
std::option::Option::Some(std::string::String::from("test-123"))
);
std::assert_eq!(
envelope.datacontenttype,
std::option::Option::Some(std::string::String::from("application/json"))
);
std::assert!(envelope.data.is_some());
}
#[test]
fn test_validate_required_attributes_success() {
let envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from("/test/source"),
std::string::String::from("com.test.event"),
);
std::assert!(envelope.validate().is_ok());
}
#[test]
fn test_validate_empty_id_fails() {
let envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from(""),
std::string::String::from("/test/source"),
std::string::String::from("com.test.event"),
);
std::assert!(envelope.validate().is_err());
}
#[test]
fn test_validate_empty_source_fails() {
let envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from(""),
std::string::String::from("com.test.event"),
);
std::assert!(envelope.validate().is_err());
}
#[test]
fn test_validate_empty_type_fails() {
let envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from("/test/source"),
std::string::String::from(""),
);
std::assert!(envelope.validate().is_err());
}
#[test]
fn test_validate_time_format_valid() {
let mut envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from("/test/source"),
std::string::String::from("com.test.event"),
);
envelope.time = std::option::Option::Some(std::string::String::from("2025-10-09T14:51:00Z"));
std::assert!(envelope.validate_time_format().is_ok());
}
#[test]
fn test_validate_time_format_invalid() {
let mut envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from("/test/source"),
std::string::String::from("com.test.event"),
);
envelope.time = std::option::Option::Some(std::string::String::from("invalid"));
std::assert!(envelope.validate_time_format().is_err());
}
#[test]
fn test_add_extension_success() {
let mut envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from("/test/source"),
std::string::String::from("com.test.event"),
);
let result = envelope.add_extension(
std::string::String::from("traceparent"),
std::string::String::from("00-trace-01"),
);
std::assert!(result.is_ok());
std::assert_eq!(
envelope.extensions.get("traceparent"),
std::option::Option::Some(&std::string::String::from("00-trace-01"))
);
}
#[test]
fn test_add_extension_rejects_ce_prefix() {
let mut envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from("/test/source"),
std::string::String::from("com.test.event"),
);
let result = envelope.add_extension(
std::string::String::from("ce-invalid"),
std::string::String::from("value"),
);
std::assert!(result.is_err());
}
#[test]
fn test_get_extension_returns_value() {
let mut envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from("/test/source"),
std::string::String::from("com.test.event"),
);
envelope
.add_extension(
std::string::String::from("tenantid"),
std::string::String::from("acme"),
)
.unwrap();
std::assert_eq!(
envelope.get_extension("tenantid"),
std::option::Option::Some(&std::string::String::from("acme"))
);
}
#[test]
fn test_get_extension_returns_none_for_missing_key() {
let envelope: CloudEventsEnvelope<std::string::String> = CloudEventsEnvelope::new(
std::string::String::from("evt-001"),
std::string::String::from("/test/source"),
std::string::String::from("com.test.event"),
);
std::assert_eq!(
envelope.get_extension("nonexistent"),
std::option::Option::None
);
}
}