Skip to main content

libpetri_debug/
debug_response.rs

1//! Responses sent from server to debug UI client via WebSocket.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::debug_command::{BreakpointConfig, EventFilter};
7
8/// Summary of a debug session.
9#[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/// Serializable token information.
20#[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/// Serializable event information.
31#[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/// Information about a place in the net structure.
43#[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/// Information about a transition in the net structure.
55#[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/// Structure of a Petri net for the debug UI.
63#[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/// Summary of an archived session.
71#[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/// Responses from server to debug UI client.
81#[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}