Skip to main content

libpetri_debug/
debug_command.rs

1//! Commands sent from debug UI client to server via WebSocket.
2
3use serde::{Deserialize, Serialize};
4
5/// Subscription mode for a debug session.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub enum SubscriptionMode {
9    Live,
10    Replay,
11}
12
13/// Breakpoint trigger types.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
16pub enum BreakpointType {
17    TransitionEnabled,
18    TransitionStart,
19    TransitionComplete,
20    TransitionFail,
21    TokenAdded,
22    TokenRemoved,
23}
24
25/// Configuration for a single breakpoint.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct BreakpointConfig {
29    pub id: String,
30    #[serde(rename = "type")]
31    pub bp_type: BreakpointType,
32    pub target: Option<String>,
33    pub enabled: bool,
34}
35
36/// Event filter for restricting which events are delivered.
37#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct EventFilter {
40    pub event_types: Option<Vec<String>>,
41    pub transition_names: Option<Vec<String>>,
42    pub place_names: Option<Vec<String>>,
43    #[serde(default)]
44    pub exclude_event_types: Option<Vec<String>>,
45    #[serde(default)]
46    pub exclude_transition_names: Option<Vec<String>>,
47    #[serde(default)]
48    pub exclude_place_names: Option<Vec<String>>,
49}
50
51impl EventFilter {
52    /// Creates a filter that matches all events.
53    pub fn all() -> Self {
54        Self::default()
55    }
56}
57
58/// Commands from debug UI client to server.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(tag = "type", rename_all = "camelCase")]
61pub enum DebugCommand {
62    ListSessions {
63        limit: Option<usize>,
64        active_only: Option<bool>,
65    },
66    Subscribe {
67        session_id: String,
68        mode: SubscriptionMode,
69        from_index: Option<usize>,
70    },
71    Unsubscribe {
72        session_id: String,
73    },
74    Seek {
75        session_id: String,
76        timestamp: String,
77    },
78    PlaybackSpeed {
79        session_id: String,
80        speed: f64,
81    },
82    Filter {
83        session_id: String,
84        filter: EventFilter,
85    },
86    Pause {
87        session_id: String,
88    },
89    Resume {
90        session_id: String,
91    },
92    StepForward {
93        session_id: String,
94    },
95    StepBackward {
96        session_id: String,
97    },
98    SetBreakpoint {
99        session_id: String,
100        breakpoint: BreakpointConfig,
101    },
102    ClearBreakpoint {
103        session_id: String,
104        breakpoint_id: String,
105    },
106    ListBreakpoints {
107        session_id: String,
108    },
109    ListArchives {
110        limit: Option<usize>,
111        prefix: Option<String>,
112    },
113    ImportArchive {
114        session_id: String,
115    },
116    UploadArchive {
117        file_name: String,
118        data: String,
119    },
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn serde_round_trip_subscribe() {
128        let cmd = DebugCommand::Subscribe {
129            session_id: "s1".into(),
130            mode: SubscriptionMode::Live,
131            from_index: Some(10),
132        };
133        let json = serde_json::to_string(&cmd).unwrap();
134        assert!(json.contains("\"type\":\"subscribe\""));
135        let back: DebugCommand = serde_json::from_str(&json).unwrap();
136        match back {
137            DebugCommand::Subscribe {
138                session_id,
139                mode,
140                from_index,
141            } => {
142                assert_eq!(session_id, "s1");
143                assert_eq!(mode, SubscriptionMode::Live);
144                assert_eq!(from_index, Some(10));
145            }
146            _ => panic!("wrong variant"),
147        }
148    }
149
150    #[test]
151    fn serde_round_trip_list_sessions() {
152        let cmd = DebugCommand::ListSessions {
153            limit: None,
154            active_only: Some(true),
155        };
156        let json = serde_json::to_string(&cmd).unwrap();
157        assert!(json.contains("\"type\":\"listSessions\""));
158        let back: DebugCommand = serde_json::from_str(&json).unwrap();
159        match back {
160            DebugCommand::ListSessions { limit, active_only } => {
161                assert!(limit.is_none());
162                assert_eq!(active_only, Some(true));
163            }
164            _ => panic!("wrong variant"),
165        }
166    }
167
168    #[test]
169    fn serde_breakpoint_config() {
170        let bp = BreakpointConfig {
171            id: "bp1".into(),
172            bp_type: BreakpointType::TransitionStart,
173            target: Some("t1".into()),
174            enabled: true,
175        };
176        let json = serde_json::to_string(&bp).unwrap();
177        assert!(json.contains("\"type\":\"TRANSITION_START\""));
178        let back: BreakpointConfig = serde_json::from_str(&json).unwrap();
179        assert_eq!(back.bp_type, BreakpointType::TransitionStart);
180    }
181
182    #[test]
183    fn serde_event_filter_all() {
184        let filter = EventFilter::all();
185        let json = serde_json::to_string(&filter).unwrap();
186        let back: EventFilter = serde_json::from_str(&json).unwrap();
187        assert!(back.event_types.is_none());
188        assert!(back.transition_names.is_none());
189        assert!(back.place_names.is_none());
190        assert!(back.exclude_event_types.is_none());
191        assert!(back.exclude_transition_names.is_none());
192        assert!(back.exclude_place_names.is_none());
193    }
194
195    #[test]
196    fn serde_event_filter_backward_compat() {
197        let json = r#"{"eventTypes":["TransitionStarted"],"transitionNames":null,"placeNames":null}"#;
198        let filter: EventFilter = serde_json::from_str(json).unwrap();
199        assert!(filter.exclude_event_types.is_none());
200        assert!(filter.exclude_transition_names.is_none());
201        assert!(filter.exclude_place_names.is_none());
202    }
203
204    #[test]
205    fn serde_event_filter_with_exclusions() {
206        let filter = EventFilter {
207            event_types: None,
208            transition_names: None,
209            place_names: None,
210            exclude_event_types: Some(vec!["LogMessage".into()]),
211            exclude_transition_names: Some(vec!["t1".into()]),
212            exclude_place_names: None,
213        };
214        let json = serde_json::to_string(&filter).unwrap();
215        let back: EventFilter = serde_json::from_str(&json).unwrap();
216        assert_eq!(back.exclude_event_types, Some(vec!["LogMessage".into()]));
217        assert_eq!(back.exclude_transition_names, Some(vec!["t1".into()]));
218        assert!(back.exclude_place_names.is_none());
219    }
220
221    #[test]
222    fn serde_all_command_variants() {
223        let cmds = vec![
224            DebugCommand::ListSessions {
225                limit: Some(10),
226                active_only: None,
227            },
228            DebugCommand::Subscribe {
229                session_id: "s1".into(),
230                mode: SubscriptionMode::Replay,
231                from_index: None,
232            },
233            DebugCommand::Unsubscribe {
234                session_id: "s1".into(),
235            },
236            DebugCommand::Seek {
237                session_id: "s1".into(),
238                timestamp: "2025-01-01T00:00:00Z".into(),
239            },
240            DebugCommand::PlaybackSpeed {
241                session_id: "s1".into(),
242                speed: 2.0,
243            },
244            DebugCommand::Filter {
245                session_id: "s1".into(),
246                filter: EventFilter::all(),
247            },
248            DebugCommand::Pause {
249                session_id: "s1".into(),
250            },
251            DebugCommand::Resume {
252                session_id: "s1".into(),
253            },
254            DebugCommand::StepForward {
255                session_id: "s1".into(),
256            },
257            DebugCommand::StepBackward {
258                session_id: "s1".into(),
259            },
260            DebugCommand::SetBreakpoint {
261                session_id: "s1".into(),
262                breakpoint: BreakpointConfig {
263                    id: "bp1".into(),
264                    bp_type: BreakpointType::TokenAdded,
265                    target: None,
266                    enabled: true,
267                },
268            },
269            DebugCommand::ClearBreakpoint {
270                session_id: "s1".into(),
271                breakpoint_id: "bp1".into(),
272            },
273            DebugCommand::ListBreakpoints {
274                session_id: "s1".into(),
275            },
276            DebugCommand::ListArchives {
277                limit: None,
278                prefix: None,
279            },
280            DebugCommand::ImportArchive {
281                session_id: "s1".into(),
282            },
283            DebugCommand::UploadArchive {
284                file_name: "test.gz".into(),
285                data: "base64data".into(),
286            },
287        ];
288        for cmd in cmds {
289            let json = serde_json::to_string(&cmd).unwrap();
290            let _back: DebugCommand = serde_json::from_str(&json).unwrap();
291        }
292    }
293}