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 {
27 data: serde_json::Value,
29 },
30
31 PiEvent {
35 data: serde_json::Value,
37 },
38
39 ListSessions,
41
42 Subscribe {
44 #[serde(skip_serializing_if = "Option::is_none")]
46 session_id: Option<SessionId>,
47 },
48
49 Unsubscribe,
51
52 Ping {
54 seq: u64,
56 },
57
58 Disconnect,
60
61 Discover,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ClientMessage {
68 pub protocol_version: ProtocolVersion,
70
71 #[serde(flatten)]
73 pub message: MessageType,
74}
75
76impl ClientMessage {
77 pub fn new(message: MessageType) -> Self {
79 Self {
80 protocol_version: ProtocolVersion::CURRENT,
81 message,
82 }
83 }
84
85 pub fn connect(client_id: Option<String>) -> Self {
87 Self::new(MessageType::Connect { client_id })
88 }
89
90 pub fn status_update(data: serde_json::Value) -> Self {
92 Self::new(MessageType::StatusUpdate { data })
93 }
94
95 pub fn hook_event(data: serde_json::Value) -> Self {
97 Self::new(MessageType::HookEvent { data })
98 }
99
100 pub fn pi_event(data: serde_json::Value) -> Self {
102 Self::new(MessageType::PiEvent { data })
103 }
104
105 pub fn list_sessions() -> Self {
107 Self::new(MessageType::ListSessions)
108 }
109
110 pub fn subscribe(session_id: Option<SessionId>) -> Self {
112 Self::new(MessageType::Subscribe { session_id })
113 }
114
115 pub fn ping(seq: u64) -> Self {
117 Self::new(MessageType::Ping { seq })
118 }
119
120 pub fn disconnect() -> Self {
122 Self::new(MessageType::Disconnect)
123 }
124
125 pub fn discover() -> Self {
127 Self::new(MessageType::Discover)
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133#[serde(tag = "type", rename_all = "snake_case")]
134pub enum DaemonMessage {
135 Connected {
137 protocol_version: ProtocolVersion,
139 client_id: String,
141 },
142
143 Rejected {
145 reason: String,
147 protocol_version: ProtocolVersion,
149 },
150
151 SessionList {
153 sessions: Vec<SessionView>,
155 },
156
157 SessionUpdated {
159 session: Box<SessionView>,
161 },
162
163 SessionRemoved {
165 session_id: SessionId,
167 },
168
169 Pong {
171 seq: u64,
173 },
174
175 Error {
177 message: String,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 code: Option<String>,
182 },
183
184 DiscoveryComplete {
186 discovered: u32,
188 failed: u32,
190 },
191}
192
193impl DaemonMessage {
194 pub fn connected(client_id: String) -> Self {
196 Self::Connected {
197 protocol_version: ProtocolVersion::CURRENT,
198 client_id,
199 }
200 }
201
202 pub fn rejected(reason: &str) -> Self {
204 Self::Rejected {
205 reason: reason.to_string(),
206 protocol_version: ProtocolVersion::CURRENT,
207 }
208 }
209
210 pub fn session_list(sessions: Vec<SessionView>) -> Self {
212 Self::SessionList { sessions }
213 }
214
215 pub fn session_updated(session: SessionView) -> Self {
217 Self::SessionUpdated {
218 session: Box::new(session),
219 }
220 }
221
222 pub fn session_removed(session_id: SessionId) -> Self {
224 Self::SessionRemoved { session_id }
225 }
226
227 pub fn pong(seq: u64) -> Self {
229 Self::Pong { seq }
230 }
231
232 pub fn error(message: &str) -> Self {
234 Self::Error {
235 message: message.to_string(),
236 code: None,
237 }
238 }
239
240 pub fn error_with_code(message: &str, code: &str) -> Self {
242 Self::Error {
243 message: message.to_string(),
244 code: Some(code.to_string()),
245 }
246 }
247
248 pub fn discovery_complete(discovered: u32, failed: u32) -> Self {
250 Self::DiscoveryComplete { discovered, failed }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_client_message_serialization() {
260 let msg = ClientMessage::ping(42);
261 let json = serde_json::to_string(&msg).unwrap();
262 assert!(json.contains("\"type\":\"ping\""));
263 assert!(json.contains("\"seq\":42"));
264 }
265
266 #[test]
267 fn test_daemon_message_serialization() {
268 let msg = DaemonMessage::connected("client-123".to_string());
269 let json = serde_json::to_string(&msg).unwrap();
270 assert!(json.contains("\"type\":\"connected\""));
271 assert!(json.contains("\"client_id\":\"client-123\""));
272 }
273
274 #[test]
275 fn test_message_roundtrip() {
276 let original = ClientMessage::subscribe(Some(SessionId::new("test-session")));
277 let json = serde_json::to_string(&original).unwrap();
278 let parsed: ClientMessage = serde_json::from_str(&json).unwrap();
279
280 match parsed.message {
281 MessageType::Subscribe { session_id } => {
282 assert_eq!(
283 session_id.map(|s| s.as_str().to_string()),
284 Some("test-session".to_string())
285 );
286 }
287 _ => panic!("Expected Subscribe message"),
288 }
289 }
290}