use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModeRequest {
pub mode: String,
pub requested_by: String,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModeChanged {
pub previous_mode: String,
pub current_mode: String,
pub nodes_started: Vec<String>,
pub nodes_stopped: Vec<String>,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ModeError {
pub mode: String,
pub error: String,
pub current_mode: String,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NodeLifecycleEvent {
pub node: String,
pub action: NodeLifecycleAction,
pub reason: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_mode: Option<String>,
#[serde(default = "default_graceful_timeout")]
pub graceful_timeout_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum NodeLifecycleAction {
Start,
Stop,
Kill,
Restart,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NodeStatusEvent {
pub node: String,
pub status: NodeLifecycleStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum NodeLifecycleStatus {
Starting,
Started,
Running,
Stopping,
Stopped,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NodeTriggerEvent {
pub node: String,
pub timestamp: u64,
}
impl crate::messages::Message for NodeTriggerEvent {}
pub const TOPIC_MODE_REQUEST: &str = "system/lifecycle/mode/request";
pub const TOPIC_MODE_CHANGED: &str = "system/lifecycle/mode/changed";
pub const TOPIC_MODE_ERROR: &str = "system/lifecycle/mode/error";
pub fn topic_node_lifecycle(node: &str) -> String {
format!("system/node/{}/lifecycle", node)
}
pub fn topic_node_status(node: &str) -> String {
format!("system/node/{}/status", node)
}
pub fn topic_node_trigger(node: &str) -> String {
format!("system/node/{}/trigger", node)
}
fn default_graceful_timeout() -> u64 {
5000
}
pub fn get_timestamp_ms() -> u64 {
crate::prelude::now_millis()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mode_request_serialization() {
let request = ModeRequest {
mode: "simulation".to_string(),
requested_by: "cli".to_string(),
timestamp: 1234567890,
};
let json = serde_json::to_string(&request).unwrap();
let deserialized: ModeRequest = serde_json::from_str(&json).unwrap();
assert_eq!(request, deserialized);
}
#[test]
fn test_mode_changed_serialization() {
let changed = ModeChanged {
previous_mode: "startup".to_string(),
current_mode: "simulation".to_string(),
nodes_started: vec!["sim-bridge".to_string(), "object-detector".to_string()],
nodes_stopped: vec!["imu".to_string(), "motor".to_string()],
timestamp: 1234567890,
};
let json = serde_json::to_string(&changed).unwrap();
let deserialized: ModeChanged = serde_json::from_str(&json).unwrap();
assert_eq!(changed, deserialized);
}
#[test]
fn test_node_lifecycle_action_serialization() {
let actions = vec![
NodeLifecycleAction::Start,
NodeLifecycleAction::Stop,
NodeLifecycleAction::Kill,
NodeLifecycleAction::Restart,
];
for action in actions {
let json = serde_json::to_string(&action).unwrap();
let deserialized: NodeLifecycleAction = serde_json::from_str(&json).unwrap();
assert_eq!(action, deserialized);
}
}
#[test]
fn test_node_lifecycle_status_serialization() {
let statuses = vec![
NodeLifecycleStatus::Starting,
NodeLifecycleStatus::Started,
NodeLifecycleStatus::Running,
NodeLifecycleStatus::Stopping,
NodeLifecycleStatus::Stopped,
NodeLifecycleStatus::Failed,
];
for status in statuses {
let json = serde_json::to_string(&status).unwrap();
let deserialized: NodeLifecycleStatus = serde_json::from_str(&json).unwrap();
assert_eq!(status, deserialized);
}
}
#[test]
fn test_topic_helpers() {
assert_eq!(topic_node_lifecycle("test-node"), "system/node/test-node/lifecycle");
assert_eq!(topic_node_status("test-node"), "system/node/test-node/status");
assert_eq!(topic_node_trigger("test-node"), "system/node/test-node/trigger");
}
#[test]
fn test_node_trigger_event_serialization() {
let event = NodeTriggerEvent {
node: "image-classifier".to_string(),
timestamp: 1234567890,
};
let json = serde_json::to_string(&event).unwrap();
let deserialized: NodeTriggerEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, deserialized);
}
#[test]
fn test_default_graceful_timeout() {
let event = NodeLifecycleEvent {
node: "test".to_string(),
action: NodeLifecycleAction::Stop,
reason: "test".to_string(),
target_mode: None,
graceful_timeout_ms: default_graceful_timeout(),
config: None,
};
assert_eq!(event.graceful_timeout_ms, 5000);
}
#[test]
fn test_get_timestamp_ms() {
let ts1 = get_timestamp_ms();
std::thread::sleep(std::time::Duration::from_millis(10));
let ts2 = get_timestamp_ms();
assert!(ts2 > ts1);
assert!(ts2 - ts1 >= 10);
}
#[test]
fn test_node_status_event_optional_fields() {
let event1 = NodeStatusEvent {
node: "test".to_string(),
status: NodeLifecycleStatus::Starting,
pid: None,
error: None,
timestamp: 1234567890,
};
let json1 = serde_json::to_string(&event1).unwrap();
assert!(!json1.contains("\"pid\""));
assert!(!json1.contains("\"error\""));
let event2 = NodeStatusEvent {
node: "test".to_string(),
status: NodeLifecycleStatus::Running,
pid: Some(12345),
error: Some("test error".to_string()),
timestamp: 1234567890,
};
let json2 = serde_json::to_string(&event2).unwrap();
assert!(json2.contains("\"pid\""));
assert!(json2.contains("\"error\""));
}
}