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 {
204 session: Box::new(session),
205 }
206 }
207
208 pub fn session_removed(session_id: SessionId) -> Self {
210 Self::SessionRemoved { session_id }
211 }
212
213 pub fn pong(seq: u64) -> Self {
215 Self::Pong { seq }
216 }
217
218 pub fn error(message: &str) -> Self {
220 Self::Error {
221 message: message.to_string(),
222 code: None,
223 }
224 }
225
226 pub fn error_with_code(message: &str, code: &str) -> Self {
228 Self::Error {
229 message: message.to_string(),
230 code: Some(code.to_string()),
231 }
232 }
233
234 pub fn discovery_complete(discovered: u32, failed: u32) -> Self {
236 Self::DiscoveryComplete { discovered, failed }
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_client_message_serialization() {
246 let msg = ClientMessage::ping(42);
247 let json = serde_json::to_string(&msg).unwrap();
248 assert!(json.contains("\"type\":\"ping\""));
249 assert!(json.contains("\"seq\":42"));
250 }
251
252 #[test]
253 fn test_daemon_message_serialization() {
254 let msg = DaemonMessage::connected("client-123".to_string());
255 let json = serde_json::to_string(&msg).unwrap();
256 assert!(json.contains("\"type\":\"connected\""));
257 assert!(json.contains("\"client_id\":\"client-123\""));
258 }
259
260 #[test]
261 fn test_message_roundtrip() {
262 let original = ClientMessage::subscribe(Some(SessionId::new("test-session")));
263 let json = serde_json::to_string(&original).unwrap();
264 let parsed: ClientMessage = serde_json::from_str(&json).unwrap();
265
266 match parsed.message {
267 MessageType::Subscribe { session_id } => {
268 assert_eq!(
269 session_id.map(|s| s.as_str().to_string()),
270 Some("test-session".to_string())
271 );
272 }
273 _ => panic!("Expected Subscribe message"),
274 }
275 }
276}