1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::debug_command::{BreakpointConfig, EventFilter};
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13#[serde(rename_all = "camelCase")]
14pub struct SessionSummary {
15 pub session_id: String,
16 pub net_name: String,
17 pub start_time: String,
18 pub active: bool,
19 pub event_count: usize,
20 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
21 pub tags: HashMap<String, String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub end_time: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub duration_ms: Option<u64>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct TokenInfo {
32 pub id: Option<String>,
33 #[serde(rename = "type")]
34 pub token_type: String,
35 pub value: Option<String>,
36 pub timestamp: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct NetEventInfo {
43 #[serde(rename = "type")]
44 pub event_type: String,
45 pub timestamp: String,
46 pub transition_name: Option<String>,
47 pub place_name: Option<String>,
48 pub details: HashMap<String, serde_json::Value>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct PlaceInfo {
55 pub name: String,
56 pub graph_id: String,
57 pub token_type: String,
58 pub is_start: bool,
59 pub is_end: bool,
60 pub is_environment: bool,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct TransitionInfo {
67 pub name: String,
68 pub graph_id: String,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(rename_all = "camelCase")]
74pub struct NetStructure {
75 pub places: Vec<PlaceInfo>,
76 pub transitions: Vec<TransitionInfo>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct ArchiveSummary {
83 pub session_id: String,
84 pub key: String,
85 pub size_bytes: u64,
86 pub last_modified: String,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
92pub enum DebugResponse {
93 SessionList {
94 sessions: Vec<SessionSummary>,
95 },
96 Subscribed {
97 session_id: String,
98 net_name: String,
99 dot_diagram: String,
100 structure: NetStructure,
101 current_marking: HashMap<String, Vec<TokenInfo>>,
102 enabled_transitions: Vec<String>,
103 in_flight_transitions: Vec<String>,
104 event_count: usize,
105 mode: String,
106 },
107 Unsubscribed {
108 session_id: String,
109 },
110 Event {
111 session_id: String,
112 index: usize,
113 event: NetEventInfo,
114 },
115 EventBatch {
116 session_id: String,
117 start_index: usize,
118 events: Vec<NetEventInfo>,
119 has_more: bool,
120 },
121 MarkingSnapshot {
122 session_id: String,
123 marking: HashMap<String, Vec<TokenInfo>>,
124 enabled_transitions: Vec<String>,
125 in_flight_transitions: Vec<String>,
126 },
127 PlaybackStateChanged {
128 session_id: String,
129 paused: bool,
130 speed: f64,
131 current_index: usize,
132 },
133 FilterApplied {
134 session_id: String,
135 filter: EventFilter,
136 },
137 BreakpointHit {
138 session_id: String,
139 breakpoint_id: String,
140 event: NetEventInfo,
141 event_index: usize,
142 },
143 BreakpointList {
144 session_id: String,
145 breakpoints: Vec<BreakpointConfig>,
146 },
147 BreakpointSet {
148 session_id: String,
149 breakpoint: BreakpointConfig,
150 },
151 BreakpointCleared {
152 session_id: String,
153 breakpoint_id: String,
154 },
155 Error {
156 code: String,
157 message: String,
158 session_id: Option<String>,
159 },
160 ArchiveList {
161 archives: Vec<ArchiveSummary>,
162 storage_available: bool,
163 },
164 ArchiveImported {
165 session_id: String,
166 net_name: String,
167 event_count: usize,
168 },
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn serde_round_trip_session_list() {
177 let resp = DebugResponse::SessionList {
178 sessions: vec![SessionSummary {
179 session_id: "s1".into(),
180 net_name: "test".into(),
181 start_time: "2025-01-01T00:00:00Z".into(),
182 active: true,
183 event_count: 42,
184 ..Default::default()
185 }],
186 };
187 let json = serde_json::to_string(&resp).unwrap();
188 assert!(json.contains("\"type\":\"sessionList\""));
189 assert!(!json.contains("\"tags\""));
191 assert!(!json.contains("\"endTime\""));
192 assert!(!json.contains("\"durationMs\""));
193 let back: DebugResponse = serde_json::from_str(&json).unwrap();
194 match back {
195 DebugResponse::SessionList { sessions } => {
196 assert_eq!(sessions.len(), 1);
197 assert_eq!(sessions[0].session_id, "s1");
198 assert!(sessions[0].tags.is_empty());
199 assert!(sessions[0].end_time.is_none());
200 assert!(sessions[0].duration_ms.is_none());
201 }
202 _ => panic!("wrong variant"),
203 }
204 }
205
206 #[test]
207 fn serde_round_trip_session_summary_with_1_6_0_fields() {
208 let mut tags = HashMap::new();
209 tags.insert("channel".to_string(), "voice".to_string());
210 tags.insert("env".to_string(), "staging".to_string());
211
212 let summary = SessionSummary {
213 session_id: "s1".into(),
214 net_name: "test".into(),
215 start_time: "1000".into(),
216 active: false,
217 event_count: 42,
218 tags,
219 end_time: Some("2500".into()),
220 duration_ms: Some(1500),
221 };
222 let json = serde_json::to_string(&summary).unwrap();
223
224 assert!(json.contains("\"tags\""));
225 assert!(json.contains("\"channel\":\"voice\""));
226 assert!(json.contains("\"endTime\":\"2500\""));
227 assert!(json.contains("\"durationMs\":1500"));
228
229 let back: SessionSummary = serde_json::from_str(&json).unwrap();
230 assert_eq!(back.session_id, "s1");
231 assert_eq!(back.end_time, Some("2500".into()));
232 assert_eq!(back.duration_ms, Some(1500));
233 assert_eq!(back.tags.get("channel"), Some(&"voice".to_string()));
234 assert_eq!(back.tags.get("env"), Some(&"staging".to_string()));
235 }
236
237 #[test]
238 fn serde_deserialize_session_summary_tolerates_missing_1_6_0_fields() {
239 let json = r#"{
241 "sessionId": "s1",
242 "netName": "test",
243 "startTime": "1000",
244 "active": true,
245 "eventCount": 0
246 }"#;
247 let summary: SessionSummary = serde_json::from_str(json).unwrap();
248 assert_eq!(summary.session_id, "s1");
249 assert!(summary.tags.is_empty());
250 assert!(summary.end_time.is_none());
251 assert!(summary.duration_ms.is_none());
252 }
253
254 #[test]
255 fn serde_round_trip_subscribed() {
256 let resp = DebugResponse::Subscribed {
257 session_id: "s1".into(),
258 net_name: "test".into(),
259 dot_diagram: "digraph {}".into(),
260 structure: NetStructure {
261 places: vec![PlaceInfo {
262 name: "p1".into(),
263 graph_id: "p_p1".into(),
264 token_type: "i32".into(),
265 is_start: true,
266 is_end: false,
267 is_environment: false,
268 }],
269 transitions: vec![TransitionInfo {
270 name: "t1".into(),
271 graph_id: "t_t1".into(),
272 }],
273 },
274 current_marking: HashMap::new(),
275 enabled_transitions: vec!["t1".into()],
276 in_flight_transitions: vec![],
277 event_count: 5,
278 mode: "live".into(),
279 };
280 let json = serde_json::to_string(&resp).unwrap();
281 assert!(json.contains("\"type\":\"subscribed\""));
282 let _back: DebugResponse = serde_json::from_str(&json).unwrap();
283 }
284
285 #[test]
286 fn serde_round_trip_error() {
287 let resp = DebugResponse::Error {
288 code: "NOT_FOUND".into(),
289 message: "Session not found".into(),
290 session_id: Some("s1".into()),
291 };
292 let json = serde_json::to_string(&resp).unwrap();
293 let back: DebugResponse = serde_json::from_str(&json).unwrap();
294 match back {
295 DebugResponse::Error {
296 code,
297 message,
298 session_id,
299 } => {
300 assert_eq!(code, "NOT_FOUND");
301 assert_eq!(message, "Session not found");
302 assert_eq!(session_id, Some("s1".into()));
303 }
304 _ => panic!("wrong variant"),
305 }
306 }
307
308 #[test]
309 fn serde_response_inline_fields_use_camelcase() {
310 let resp = DebugResponse::Subscribed {
313 session_id: "s1".into(),
314 net_name: "test".into(),
315 dot_diagram: "digraph {}".into(),
316 structure: NetStructure {
317 places: vec![],
318 transitions: vec![],
319 },
320 current_marking: HashMap::new(),
321 enabled_transitions: vec!["t1".into()],
322 in_flight_transitions: vec![],
323 event_count: 5,
324 mode: "live".into(),
325 };
326 let json = serde_json::to_string(&resp).unwrap();
327 assert!(json.contains("\"sessionId\":\"s1\""));
328 assert!(json.contains("\"netName\":\"test\""));
329 assert!(json.contains("\"dotDiagram\""));
330 assert!(json.contains("\"currentMarking\""));
331 assert!(json.contains("\"enabledTransitions\""));
332 assert!(json.contains("\"inFlightTransitions\""));
333 assert!(json.contains("\"eventCount\":5"));
334 assert!(!json.contains("\"session_id\""));
335 assert!(!json.contains("\"net_name\""));
336 assert!(!json.contains("\"event_count\""));
337 }
338
339 #[test]
340 fn serde_event_batch_camelcase_interop() {
341 let json = r#"{"type":"eventBatch","sessionId":"s1","startIndex":0,"events":[],"hasMore":true}"#;
343 let resp: DebugResponse = serde_json::from_str(json).unwrap();
344 match resp {
345 DebugResponse::EventBatch {
346 session_id,
347 start_index,
348 has_more,
349 ..
350 } => {
351 assert_eq!(session_id, "s1");
352 assert_eq!(start_index, 0);
353 assert!(has_more);
354 }
355 _ => panic!("wrong variant"),
356 }
357 }
358
359 #[test]
360 fn serde_all_response_variants() {
361 let responses: Vec<DebugResponse> = vec![
362 DebugResponse::SessionList { sessions: vec![] },
363 DebugResponse::Unsubscribed {
364 session_id: "s1".into(),
365 },
366 DebugResponse::Event {
367 session_id: "s1".into(),
368 index: 0,
369 event: NetEventInfo {
370 event_type: "TransitionStarted".into(),
371 timestamp: "2025-01-01T00:00:00Z".into(),
372 transition_name: Some("t1".into()),
373 place_name: None,
374 details: HashMap::new(),
375 },
376 },
377 DebugResponse::EventBatch {
378 session_id: "s1".into(),
379 start_index: 0,
380 events: vec![],
381 has_more: false,
382 },
383 DebugResponse::MarkingSnapshot {
384 session_id: "s1".into(),
385 marking: HashMap::new(),
386 enabled_transitions: vec![],
387 in_flight_transitions: vec![],
388 },
389 DebugResponse::PlaybackStateChanged {
390 session_id: "s1".into(),
391 paused: true,
392 speed: 1.0,
393 current_index: 0,
394 },
395 DebugResponse::FilterApplied {
396 session_id: "s1".into(),
397 filter: EventFilter::all(),
398 },
399 DebugResponse::BreakpointList {
400 session_id: "s1".into(),
401 breakpoints: vec![],
402 },
403 DebugResponse::BreakpointCleared {
404 session_id: "s1".into(),
405 breakpoint_id: "bp1".into(),
406 },
407 DebugResponse::Error {
408 code: "ERR".into(),
409 message: "msg".into(),
410 session_id: None,
411 },
412 DebugResponse::ArchiveList {
413 archives: vec![],
414 storage_available: false,
415 },
416 DebugResponse::ArchiveImported {
417 session_id: "s1".into(),
418 net_name: "test".into(),
419 event_count: 10,
420 },
421 ];
422 for resp in responses {
423 let json = serde_json::to_string(&resp).unwrap();
424 let _back: DebugResponse = serde_json::from_str(&json).unwrap();
425 }
426 }
427}