1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::debug_command::{BreakpointConfig, EventFilter};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct SessionSummary {
12 pub session_id: String,
13 pub net_name: String,
14 pub start_time: String,
15 pub active: bool,
16 pub event_count: usize,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct TokenInfo {
23 pub id: Option<String>,
24 #[serde(rename = "type")]
25 pub token_type: String,
26 pub value: Option<String>,
27 pub timestamp: Option<String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct NetEventInfo {
34 #[serde(rename = "type")]
35 pub event_type: String,
36 pub timestamp: String,
37 pub transition_name: Option<String>,
38 pub place_name: Option<String>,
39 pub details: HashMap<String, serde_json::Value>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct PlaceInfo {
46 pub name: String,
47 pub graph_id: String,
48 pub token_type: String,
49 pub is_start: bool,
50 pub is_end: bool,
51 pub is_environment: bool,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct TransitionInfo {
58 pub name: String,
59 pub graph_id: String,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct NetStructure {
66 pub places: Vec<PlaceInfo>,
67 pub transitions: Vec<TransitionInfo>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct ArchiveSummary {
74 pub session_id: String,
75 pub key: String,
76 pub size_bytes: u64,
77 pub last_modified: String,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(tag = "type", rename_all = "camelCase")]
83pub enum DebugResponse {
84 SessionList {
85 sessions: Vec<SessionSummary>,
86 },
87 Subscribed {
88 session_id: String,
89 net_name: String,
90 dot_diagram: String,
91 structure: NetStructure,
92 current_marking: HashMap<String, Vec<TokenInfo>>,
93 enabled_transitions: Vec<String>,
94 in_flight_transitions: Vec<String>,
95 event_count: usize,
96 mode: String,
97 },
98 Unsubscribed {
99 session_id: String,
100 },
101 Event {
102 session_id: String,
103 index: usize,
104 event: NetEventInfo,
105 },
106 EventBatch {
107 session_id: String,
108 start_index: usize,
109 events: Vec<NetEventInfo>,
110 has_more: bool,
111 },
112 MarkingSnapshot {
113 session_id: String,
114 marking: HashMap<String, Vec<TokenInfo>>,
115 enabled_transitions: Vec<String>,
116 in_flight_transitions: Vec<String>,
117 },
118 PlaybackStateChanged {
119 session_id: String,
120 paused: bool,
121 speed: f64,
122 current_index: usize,
123 },
124 FilterApplied {
125 session_id: String,
126 filter: EventFilter,
127 },
128 BreakpointHit {
129 session_id: String,
130 breakpoint_id: String,
131 event: NetEventInfo,
132 event_index: usize,
133 },
134 BreakpointList {
135 session_id: String,
136 breakpoints: Vec<BreakpointConfig>,
137 },
138 BreakpointSet {
139 session_id: String,
140 breakpoint: BreakpointConfig,
141 },
142 BreakpointCleared {
143 session_id: String,
144 breakpoint_id: String,
145 },
146 Error {
147 code: String,
148 message: String,
149 session_id: Option<String>,
150 },
151 ArchiveList {
152 archives: Vec<ArchiveSummary>,
153 storage_available: bool,
154 },
155 ArchiveImported {
156 session_id: String,
157 net_name: String,
158 event_count: usize,
159 },
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn serde_round_trip_session_list() {
168 let resp = DebugResponse::SessionList {
169 sessions: vec![SessionSummary {
170 session_id: "s1".into(),
171 net_name: "test".into(),
172 start_time: "2025-01-01T00:00:00Z".into(),
173 active: true,
174 event_count: 42,
175 }],
176 };
177 let json = serde_json::to_string(&resp).unwrap();
178 assert!(json.contains("\"type\":\"sessionList\""));
179 let back: DebugResponse = serde_json::from_str(&json).unwrap();
180 match back {
181 DebugResponse::SessionList { sessions } => {
182 assert_eq!(sessions.len(), 1);
183 assert_eq!(sessions[0].session_id, "s1");
184 }
185 _ => panic!("wrong variant"),
186 }
187 }
188
189 #[test]
190 fn serde_round_trip_subscribed() {
191 let resp = DebugResponse::Subscribed {
192 session_id: "s1".into(),
193 net_name: "test".into(),
194 dot_diagram: "digraph {}".into(),
195 structure: NetStructure {
196 places: vec![PlaceInfo {
197 name: "p1".into(),
198 graph_id: "p_p1".into(),
199 token_type: "i32".into(),
200 is_start: true,
201 is_end: false,
202 is_environment: false,
203 }],
204 transitions: vec![TransitionInfo {
205 name: "t1".into(),
206 graph_id: "t_t1".into(),
207 }],
208 },
209 current_marking: HashMap::new(),
210 enabled_transitions: vec!["t1".into()],
211 in_flight_transitions: vec![],
212 event_count: 5,
213 mode: "live".into(),
214 };
215 let json = serde_json::to_string(&resp).unwrap();
216 assert!(json.contains("\"type\":\"subscribed\""));
217 let _back: DebugResponse = serde_json::from_str(&json).unwrap();
218 }
219
220 #[test]
221 fn serde_round_trip_error() {
222 let resp = DebugResponse::Error {
223 code: "NOT_FOUND".into(),
224 message: "Session not found".into(),
225 session_id: Some("s1".into()),
226 };
227 let json = serde_json::to_string(&resp).unwrap();
228 let back: DebugResponse = serde_json::from_str(&json).unwrap();
229 match back {
230 DebugResponse::Error {
231 code,
232 message,
233 session_id,
234 } => {
235 assert_eq!(code, "NOT_FOUND");
236 assert_eq!(message, "Session not found");
237 assert_eq!(session_id, Some("s1".into()));
238 }
239 _ => panic!("wrong variant"),
240 }
241 }
242
243 #[test]
244 fn serde_all_response_variants() {
245 let responses: Vec<DebugResponse> = vec![
246 DebugResponse::SessionList { sessions: vec![] },
247 DebugResponse::Unsubscribed {
248 session_id: "s1".into(),
249 },
250 DebugResponse::Event {
251 session_id: "s1".into(),
252 index: 0,
253 event: NetEventInfo {
254 event_type: "TransitionStarted".into(),
255 timestamp: "2025-01-01T00:00:00Z".into(),
256 transition_name: Some("t1".into()),
257 place_name: None,
258 details: HashMap::new(),
259 },
260 },
261 DebugResponse::EventBatch {
262 session_id: "s1".into(),
263 start_index: 0,
264 events: vec![],
265 has_more: false,
266 },
267 DebugResponse::MarkingSnapshot {
268 session_id: "s1".into(),
269 marking: HashMap::new(),
270 enabled_transitions: vec![],
271 in_flight_transitions: vec![],
272 },
273 DebugResponse::PlaybackStateChanged {
274 session_id: "s1".into(),
275 paused: true,
276 speed: 1.0,
277 current_index: 0,
278 },
279 DebugResponse::FilterApplied {
280 session_id: "s1".into(),
281 filter: EventFilter::all(),
282 },
283 DebugResponse::BreakpointList {
284 session_id: "s1".into(),
285 breakpoints: vec![],
286 },
287 DebugResponse::BreakpointCleared {
288 session_id: "s1".into(),
289 breakpoint_id: "bp1".into(),
290 },
291 DebugResponse::Error {
292 code: "ERR".into(),
293 message: "msg".into(),
294 session_id: None,
295 },
296 DebugResponse::ArchiveList {
297 archives: vec![],
298 storage_available: false,
299 },
300 DebugResponse::ArchiveImported {
301 session_id: "s1".into(),
302 net_name: "test".into(),
303 event_count: 10,
304 },
305 ];
306 for resp in responses {
307 let json = serde_json::to_string(&resp).unwrap();
308 let _back: DebugResponse = serde_json::from_str(&json).unwrap();
309 }
310 }
311}