//! Message Type Definitions //! //! This module defines the core message types and traits for inter-service //! communication in Attune. All messages follow a standard envelope format //! with headers and payload. use chrono::{DateTime, Utc}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value as JsonValue; use uuid::Uuid; use crate::models::Id; /// Message trait that all messages must implement pub trait Message: Serialize + for<'de> Deserialize<'de> + Send + Sync { /// Get the message type identifier fn message_type(&self) -> MessageType; /// Get the routing key for this message fn routing_key(&self) -> String { self.message_type().routing_key() } /// Get the exchange name for this message fn exchange(&self) -> String { self.message_type().exchange() } /// Serialize message to JSON fn to_json(&self) -> Result { serde_json::to_string(self) } /// Deserialize message from JSON fn from_json(json: &str) -> Result where Self: Sized, { serde_json::from_str(json) } } /// Message type identifier #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum MessageType { /// Event created by sensor EventCreated, /// Enforcement created (rule triggered) EnforcementCreated, /// Execution requested ExecutionRequested, /// Execution status changed ExecutionStatusChanged, /// Execution completed ExecutionCompleted, /// Inquiry created (human input needed) InquiryCreated, /// Inquiry responded InquiryResponded, /// Notification created NotificationCreated, /// Rule created RuleCreated, /// Rule enabled RuleEnabled, /// Rule disabled RuleDisabled, /// Pack registered or installed (triggers runtime environment setup in workers) PackRegistered, /// Execution cancel requested (sent to worker to gracefully stop a running execution) ExecutionCancelRequested, } impl MessageType { /// Get the routing key for this message type pub fn routing_key(&self) -> String { match self { Self::EventCreated => "event.created".to_string(), Self::EnforcementCreated => "enforcement.created".to_string(), Self::ExecutionRequested => "execution.requested".to_string(), Self::ExecutionStatusChanged => "execution.status.changed".to_string(), Self::ExecutionCompleted => "execution.completed".to_string(), Self::InquiryCreated => "inquiry.created".to_string(), Self::InquiryResponded => "inquiry.responded".to_string(), Self::NotificationCreated => "notification.created".to_string(), Self::RuleCreated => "rule.created".to_string(), Self::RuleEnabled => "rule.enabled".to_string(), Self::RuleDisabled => "rule.disabled".to_string(), Self::PackRegistered => "pack.registered".to_string(), Self::ExecutionCancelRequested => "execution.cancel".to_string(), } } /// Get the exchange name for this message type pub fn exchange(&self) -> String { match self { Self::EventCreated => "attune.events".to_string(), Self::EnforcementCreated => "attune.executions".to_string(), Self::ExecutionRequested | Self::ExecutionStatusChanged | Self::ExecutionCompleted => { "attune.executions".to_string() } Self::InquiryCreated | Self::InquiryResponded => "attune.executions".to_string(), Self::NotificationCreated => "attune.notifications".to_string(), Self::RuleCreated | Self::RuleEnabled | Self::RuleDisabled => { "attune.events".to_string() } Self::PackRegistered => "attune.events".to_string(), Self::ExecutionCancelRequested => "attune.executions".to_string(), } } /// Get the message type as a string pub fn as_str(&self) -> &'static str { match self { Self::EventCreated => "EventCreated", Self::EnforcementCreated => "EnforcementCreated", Self::ExecutionRequested => "ExecutionRequested", Self::ExecutionStatusChanged => "ExecutionStatusChanged", Self::ExecutionCompleted => "ExecutionCompleted", Self::InquiryCreated => "InquiryCreated", Self::InquiryResponded => "InquiryResponded", Self::NotificationCreated => "NotificationCreated", Self::RuleCreated => "RuleCreated", Self::RuleEnabled => "RuleEnabled", Self::RuleDisabled => "RuleDisabled", Self::PackRegistered => "PackRegistered", Self::ExecutionCancelRequested => "ExecutionCancelRequested", } } } /// Deserialize a UUID, substituting a freshly-generated one when the value is /// null or absent. This keeps envelope parsing tolerant of messages that were /// hand-crafted or produced by older tooling. fn deserialize_uuid_default<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let opt: Option = Option::deserialize(deserializer)?; Ok(opt.unwrap_or_else(Uuid::new_v4)) } /// Message envelope that wraps all messages with metadata #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MessageEnvelope where T: Clone, { /// Unique message identifier #[serde( default = "Uuid::new_v4", deserialize_with = "deserialize_uuid_default" )] pub message_id: Uuid, /// Correlation ID for tracing related messages #[serde( default = "Uuid::new_v4", deserialize_with = "deserialize_uuid_default" )] pub correlation_id: Uuid, /// Message type pub message_type: MessageType, /// Message version (for backwards compatibility) #[serde(default = "default_version")] pub version: String, /// Timestamp when message was created pub timestamp: DateTime, /// Message headers #[serde(default)] pub headers: MessageHeaders, /// Message payload pub payload: T, } impl MessageEnvelope where T: Clone + Serialize + for<'de> Deserialize<'de>, { /// Create a new message envelope pub fn new(message_type: MessageType, payload: T) -> Self { let message_id = Uuid::new_v4(); Self { message_id, correlation_id: message_id, // Default to message_id, can be overridden message_type, version: "1.0".to_string(), timestamp: Utc::now(), headers: MessageHeaders::default(), payload, } } /// Set correlation ID for message tracing pub fn with_correlation_id(mut self, correlation_id: Uuid) -> Self { self.correlation_id = correlation_id; self } /// Set source service pub fn with_source(mut self, source: impl Into) -> Self { self.headers.source_service = Some(source.into()); self } /// Set trace ID pub fn with_trace_id(mut self, trace_id: Uuid) -> Self { self.headers.trace_id = Some(trace_id); self } /// Increment retry count pub fn increment_retry(&mut self) { self.headers.retry_count += 1; } /// Serialize to JSON string pub fn to_json(&self) -> Result { serde_json::to_string(self) } /// Deserialize from JSON string pub fn from_json(json: &str) -> Result { serde_json::from_str(json) } /// Serialize to JSON bytes pub fn to_bytes(&self) -> Result, serde_json::Error> { serde_json::to_vec(self) } /// Deserialize from JSON bytes pub fn from_bytes(bytes: &[u8]) -> Result { serde_json::from_slice(bytes) } } /// Message headers for metadata and tracing #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct MessageHeaders { /// Number of times this message has been retried #[serde(default)] pub retry_count: u32, /// Source service that generated this message #[serde(skip_serializing_if = "Option::is_none")] pub source_service: Option, /// Trace ID for distributed tracing #[serde(skip_serializing_if = "Option::is_none")] pub trace_id: Option, /// Additional custom headers #[serde(flatten)] pub custom: JsonValue, } impl MessageHeaders { /// Create new headers pub fn new() -> Self { Self::default() } /// Create headers with source service pub fn with_source(source: impl Into) -> Self { Self { source_service: Some(source.into()), ..Default::default() } } } fn default_version() -> String { "1.0".to_string() } // ============================================================================ // Message Payload Definitions // ============================================================================ /// Payload for EventCreated message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EventCreatedPayload { /// Event ID pub event_id: Id, /// Trigger ID (may be None if trigger was deleted) pub trigger_id: Option, /// Trigger reference pub trigger_ref: String, /// Sensor ID that generated the event (None for system events) pub sensor_id: Option, /// Sensor reference (None for system events) pub sensor_ref: Option, /// Event payload data pub payload: JsonValue, /// Configuration snapshot pub config: Option, } /// Payload for EnforcementCreated message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EnforcementCreatedPayload { /// Enforcement ID pub enforcement_id: Id, /// Rule ID (may be None if rule was deleted) pub rule_id: Option, /// Rule reference pub rule_ref: String, /// Event ID that triggered this enforcement pub event_id: Option, /// Trigger reference pub trigger_ref: String, /// Event payload for rule evaluation pub payload: JsonValue, } /// Payload for ExecutionRequested message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutionRequestedPayload { /// Execution ID pub execution_id: Id, /// Action ID (may be None if action was deleted) pub action_id: Option, /// Action reference pub action_ref: String, /// Parent execution ID (for workflows) pub parent_id: Option, /// Enforcement ID that created this execution pub enforcement_id: Option, /// Execution configuration/parameters pub config: Option, } /// Payload for ExecutionStatusChanged message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutionStatusChangedPayload { /// Execution ID pub execution_id: Id, /// Action reference pub action_ref: String, /// Previous status pub previous_status: String, /// New status pub new_status: String, /// Status change timestamp pub changed_at: DateTime, } /// Payload for ExecutionCompleted message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutionCompletedPayload { /// Execution ID pub execution_id: Id, /// Action ID (needed for queue notification) pub action_id: Id, /// Action reference pub action_ref: String, /// Execution status (completed, failed, timeout, etc.) pub status: String, /// Execution result data pub result: Option, /// Completion timestamp pub completed_at: DateTime, } /// Payload for InquiryCreated message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InquiryCreatedPayload { /// Inquiry ID pub inquiry_id: Id, /// Execution ID that created this inquiry pub execution_id: Id, /// Prompt text for the user pub prompt: String, /// Response schema (optional) pub response_schema: Option, /// User/identity assigned to respond (optional) pub assigned_to: Option, /// Timeout timestamp (optional) pub timeout_at: Option>, } /// Payload for InquiryResponded message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InquiryRespondedPayload { /// Inquiry ID pub inquiry_id: Id, /// Execution ID pub execution_id: Id, /// Response data pub response: JsonValue, /// User/identity that responded pub responded_by: Option, /// Response timestamp pub responded_at: DateTime, } /// Payload for NotificationCreated message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NotificationCreatedPayload { /// Notification ID pub notification_id: Id, /// Notification channel pub channel: String, /// Entity type (execution, inquiry, etc.) pub entity_type: String, /// Entity identifier pub entity: String, /// Activity description pub activity: String, /// Notification content pub content: Option, } /// Payload for RuleCreated message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RuleCreatedPayload { /// Rule ID pub rule_id: Id, /// Rule reference pub rule_ref: String, /// Trigger ID pub trigger_id: Option, /// Trigger reference pub trigger_ref: String, /// Action ID pub action_id: Option, /// Action reference pub action_ref: String, /// Trigger parameters (from rule.trigger_params) pub trigger_params: Option, /// Whether rule is enabled pub enabled: bool, } /// Payload for RuleEnabled message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RuleEnabledPayload { /// Rule ID pub rule_id: Id, /// Rule reference pub rule_ref: String, /// Trigger reference pub trigger_ref: String, /// Trigger parameters (from rule.trigger_params) pub trigger_params: Option, } /// Payload for RuleDisabled message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RuleDisabledPayload { /// Rule ID pub rule_id: Id, /// Rule reference pub rule_ref: String, /// Trigger reference pub trigger_ref: String, } /// Payload for PackRegistered message /// /// Published when a pack is registered or installed so that workers can /// proactively create runtime environments (virtualenvs, node_modules, etc.) /// instead of waiting until the first execution. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PackRegisteredPayload { /// Pack ID pub pack_id: Id, /// Pack reference (e.g., "python_example") pub pack_ref: String, /// Pack version pub version: String, /// Runtime names that require environment setup (lowercase, e.g., ["python"]) pub runtime_names: Vec, } /// Payload for ExecutionCancelRequested message /// /// Sent by the API to the worker that is running a specific execution, /// instructing it to gracefully terminate the process (SIGINT, then SIGTERM /// after a grace period). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExecutionCancelRequestedPayload { /// Execution ID to cancel pub execution_id: Id, /// Worker ID that should handle this cancel (used for routing) pub worker_id: Id, } #[cfg(test)] mod tests { use super::*; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct TestPayload { data: String, } #[test] fn test_message_envelope_creation() { let payload = TestPayload { data: "test".to_string(), }; let envelope = MessageEnvelope::new(MessageType::EventCreated, payload.clone()); assert_eq!(envelope.message_type, MessageType::EventCreated); assert_eq!(envelope.payload.data, "test"); assert_eq!(envelope.version, "1.0"); assert_eq!(envelope.message_id, envelope.correlation_id); } #[test] fn test_message_envelope_with_correlation_id() { let payload = TestPayload { data: "test".to_string(), }; let correlation_id = Uuid::new_v4(); let envelope = MessageEnvelope::new(MessageType::EventCreated, payload) .with_correlation_id(correlation_id); assert_eq!(envelope.correlation_id, correlation_id); assert_ne!(envelope.message_id, envelope.correlation_id); } #[test] fn test_message_envelope_serialization() { let payload = TestPayload { data: "test".to_string(), }; let envelope = MessageEnvelope::new(MessageType::EventCreated, payload); let json = envelope.to_json().unwrap(); assert!(json.contains("EventCreated")); assert!(json.contains("test")); let deserialized: MessageEnvelope = MessageEnvelope::from_json(&json).unwrap(); assert_eq!(deserialized.message_id, envelope.message_id); assert_eq!(deserialized.payload.data, "test"); } #[test] fn test_message_type_routing_key() { assert_eq!(MessageType::EventCreated.routing_key(), "event.created"); assert_eq!( MessageType::ExecutionRequested.routing_key(), "execution.requested" ); } #[test] fn test_message_type_exchange() { assert_eq!(MessageType::EventCreated.exchange(), "attune.events"); assert_eq!( MessageType::ExecutionRequested.exchange(), "attune.executions" ); assert_eq!( MessageType::NotificationCreated.exchange(), "attune.notifications" ); } #[test] fn test_retry_increment() { let payload = TestPayload { data: "test".to_string(), }; let mut envelope = MessageEnvelope::new(MessageType::EventCreated, payload); assert_eq!(envelope.headers.retry_count, 0); envelope.increment_retry(); assert_eq!(envelope.headers.retry_count, 1); envelope.increment_retry(); assert_eq!(envelope.headers.retry_count, 2); } #[test] fn test_message_headers_with_source() { let headers = MessageHeaders::with_source("api-service"); assert_eq!(headers.source_service, Some("api-service".to_string())); } #[test] fn test_envelope_with_source_and_trace() { let payload = TestPayload { data: "test".to_string(), }; let trace_id = Uuid::new_v4(); let envelope = MessageEnvelope::new(MessageType::EventCreated, payload) .with_source("api-service") .with_trace_id(trace_id); assert_eq!( envelope.headers.source_service, Some("api-service".to_string()) ); assert_eq!(envelope.headers.trace_id, Some(trace_id)); } }