re-uploading work
This commit is contained in:
545
crates/common/src/mq/messages.rs
Normal file
545
crates/common/src/mq/messages.rs
Normal file
@@ -0,0 +1,545 @@
|
||||
//! 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, 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<String, serde_json::Error> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
|
||||
/// Deserialize message from JSON
|
||||
fn from_json(json: &str) -> Result<Self, serde_json::Error>
|
||||
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,
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Message envelope that wraps all messages with metadata
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MessageEnvelope<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
/// Unique message identifier
|
||||
pub message_id: Uuid,
|
||||
|
||||
/// Correlation ID for tracing related messages
|
||||
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<Utc>,
|
||||
|
||||
/// Message headers
|
||||
#[serde(default)]
|
||||
pub headers: MessageHeaders,
|
||||
|
||||
/// Message payload
|
||||
pub payload: T,
|
||||
}
|
||||
|
||||
impl<T> MessageEnvelope<T>
|
||||
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<String>) -> 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<String, serde_json::Error> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
|
||||
/// Deserialize from JSON string
|
||||
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
|
||||
serde_json::from_str(json)
|
||||
}
|
||||
|
||||
/// Serialize to JSON bytes
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
|
||||
serde_json::to_vec(self)
|
||||
}
|
||||
|
||||
/// Deserialize from JSON bytes
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
||||
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<String>,
|
||||
|
||||
/// Trace ID for distributed tracing
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub trace_id: Option<Uuid>,
|
||||
|
||||
/// 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<String>) -> 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<Id>,
|
||||
/// Trigger reference
|
||||
pub trigger_ref: String,
|
||||
/// Sensor ID that generated the event (None for system events)
|
||||
pub sensor_id: Option<Id>,
|
||||
/// Sensor reference (None for system events)
|
||||
pub sensor_ref: Option<String>,
|
||||
/// Event payload data
|
||||
pub payload: JsonValue,
|
||||
/// Configuration snapshot
|
||||
pub config: Option<JsonValue>,
|
||||
}
|
||||
|
||||
/// 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<Id>,
|
||||
/// Rule reference
|
||||
pub rule_ref: String,
|
||||
/// Event ID that triggered this enforcement
|
||||
pub event_id: Option<Id>,
|
||||
/// 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<Id>,
|
||||
/// Action reference
|
||||
pub action_ref: String,
|
||||
/// Parent execution ID (for workflows)
|
||||
pub parent_id: Option<Id>,
|
||||
/// Enforcement ID that created this execution
|
||||
pub enforcement_id: Option<Id>,
|
||||
/// Execution configuration/parameters
|
||||
pub config: Option<JsonValue>,
|
||||
}
|
||||
|
||||
/// 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<Utc>,
|
||||
}
|
||||
|
||||
/// 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<JsonValue>,
|
||||
/// Completion timestamp
|
||||
pub completed_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 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<JsonValue>,
|
||||
/// User/identity assigned to respond (optional)
|
||||
pub assigned_to: Option<Id>,
|
||||
/// Timeout timestamp (optional)
|
||||
pub timeout_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// 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<Id>,
|
||||
/// Response timestamp
|
||||
pub responded_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 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<JsonValue>,
|
||||
}
|
||||
|
||||
/// 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<Id>,
|
||||
/// Trigger reference
|
||||
pub trigger_ref: String,
|
||||
/// Action ID
|
||||
pub action_id: Option<Id>,
|
||||
/// Action reference
|
||||
pub action_ref: String,
|
||||
/// Trigger parameters (from rule.trigger_params)
|
||||
pub trigger_params: Option<JsonValue>,
|
||||
/// 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<JsonValue>,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[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<TestPayload> = 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user