1use crate::version::ProtocolVersion;
4use atm_core::{SessionId, SessionView};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type", rename_all = "snake_case")]
10pub enum MessageType {
11 Connect {
13 #[serde(skip_serializing_if = "Option::is_none")]
15 client_id: Option<String>,
16 },
17
18 StatusUpdate {
20 data: serde_json::Value,
22 },
23
24 HookEvent {
26 data: serde_json::Value,
28 },
29
30 ListSessions,
32
33 Subscribe {
35 #[serde(skip_serializing_if = "Option::is_none")]
37 session_id: Option<SessionId>,
38 },
39
40 Unsubscribe,
42
43 Ping {
45 seq: u64,
47 },
48
49 Disconnect,
51
52 Discover,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ClientMessage {
59 pub protocol_version: ProtocolVersion,
61
62 #[serde(flatten)]
64 pub message: MessageType,
65}
66
67impl ClientMessage {
68 pub fn new(message: MessageType) -> Self {
70 Self {
71 protocol_version: ProtocolVersion::CURRENT,
72 message,
73 }
74 }
75
76 pub fn connect(client_id: Option<String>) -> Self {
78 Self::new(MessageType::Connect { client_id })
79 }
80
81 pub fn status_update(data: serde_json::Value) -> Self {
83 Self::new(MessageType::StatusUpdate { data })
84 }
85
86 pub fn hook_event(data: serde_json::Value) -> Self {
88 Self::new(MessageType::HookEvent { data })
89 }
90
91 pub fn list_sessions() -> Self {
93 Self::new(MessageType::ListSessions)
94 }
95
96 pub fn subscribe(session_id: Option<SessionId>) -> Self {
98 Self::new(MessageType::Subscribe { session_id })
99 }
100
101 pub fn ping(seq: u64) -> Self {
103 Self::new(MessageType::Ping { seq })
104 }
105
106 pub fn disconnect() -> Self {
108 Self::new(MessageType::Disconnect)
109 }
110
111 pub fn discover() -> Self {
113 Self::new(MessageType::Discover)
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(tag = "type", rename_all = "snake_case")]
120pub enum DaemonMessage {
121 Connected {
123 protocol_version: ProtocolVersion,
125 client_id: String,
127 },
128
129 Rejected {
131 reason: String,
133 protocol_version: ProtocolVersion,
135 },
136
137 SessionList {
139 sessions: Vec<SessionView>,
141 },
142
143 SessionUpdated {
145 session: Box<SessionView>,
147 },
148
149 SessionRemoved {
151 session_id: SessionId,
153 },
154
155 Pong {
157 seq: u64,
159 },
160
161 Error {
163 message: String,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 code: Option<String>,
168 },
169
170 DiscoveryComplete {
172 discovered: u32,
174 failed: u32,
176 },
177}
178
179impl DaemonMessage {
180 pub fn connected(client_id: String) -> Self {
182 Self::Connected {
183 protocol_version: ProtocolVersion::CURRENT,
184 client_id,
185 }
186 }
187
188 pub fn rejected(reason: &str) -> Self {
190 Self::Rejected {
191 reason: reason.to_string(),
192 protocol_version: ProtocolVersion::CURRENT,
193 }
194 }
195
196 pub fn session_list(sessions: Vec<SessionView>) -> Self {
198 Self::SessionList { sessions }
199 }
200
201 pub fn session_updated(session: SessionView) -> Self {
203 Self::SessionUpdated { session: Box::new(session) }
204 }
205
206 pub fn session_removed(session_id: SessionId) -> Self {
208 Self::SessionRemoved { session_id }
209 }
210
211 pub fn pong(seq: u64) -> Self {
213 Self::Pong { seq }
214 }
215
216 pub fn error(message: &str) -> Self {
218 Self::Error {
219 message: message.to_string(),
220 code: None,
221 }
222 }
223
224 pub fn error_with_code(message: &str, code: &str) -> Self {
226 Self::Error {
227 message: message.to_string(),
228 code: Some(code.to_string()),
229 }
230 }
231
232 pub fn discovery_complete(discovered: u32, failed: u32) -> Self {
234 Self::DiscoveryComplete { discovered, failed }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_client_message_serialization() {
244 let msg = ClientMessage::ping(42);
245 let json = serde_json::to_string(&msg).unwrap();
246 assert!(json.contains("\"type\":\"ping\""));
247 assert!(json.contains("\"seq\":42"));
248 }
249
250 #[test]
251 fn test_daemon_message_serialization() {
252 let msg = DaemonMessage::connected("client-123".to_string());
253 let json = serde_json::to_string(&msg).unwrap();
254 assert!(json.contains("\"type\":\"connected\""));
255 assert!(json.contains("\"client_id\":\"client-123\""));
256 }
257
258 #[test]
259 fn test_message_roundtrip() {
260 let original = ClientMessage::subscribe(Some(SessionId::new("test-session")));
261 let json = serde_json::to_string(&original).unwrap();
262 let parsed: ClientMessage = serde_json::from_str(&json).unwrap();
263
264 match parsed.message {
265 MessageType::Subscribe { session_id } => {
266 assert_eq!(session_id.map(|s| s.as_str().to_string()), Some("test-session".to_string()));
267 }
268 _ => panic!("Expected Subscribe message"),
269 }
270 }
271}