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}
44
45impl EventFilter {
46    /// Creates a filter that matches all events.
47    pub fn all() -> Self {
48        Self::default()
49    }
50}
51
52/// Commands from debug UI client to server.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(tag = "type", rename_all = "camelCase")]
55pub enum DebugCommand {
56    ListSessions {
57        limit: Option<usize>,
58        active_only: Option<bool>,
59    },
60    Subscribe {
61        session_id: String,
62        mode: SubscriptionMode,
63        from_index: Option<usize>,
64    },
65    Unsubscribe {
66        session_id: String,
67    },
68    Seek {
69        session_id: String,
70        timestamp: String,
71    },
72    PlaybackSpeed {
73        session_id: String,
74        speed: f64,
75    },
76    Filter {
77        session_id: String,
78        filter: EventFilter,
79    },
80    Pause {
81        session_id: String,
82    },
83    Resume {
84        session_id: String,
85    },
86    StepForward {
87        session_id: String,
88    },
89    StepBackward {
90        session_id: String,
91    },
92    SetBreakpoint {
93        session_id: String,
94        breakpoint: BreakpointConfig,
95    },
96    ClearBreakpoint {
97        session_id: String,
98        breakpoint_id: String,
99    },
100    ListBreakpoints {
101        session_id: String,
102    },
103    ListArchives {
104        limit: Option<usize>,
105        prefix: Option<String>,
106    },
107    ImportArchive {
108        session_id: String,
109    },
110    UploadArchive {
111        file_name: String,
112        data: String,
113    },
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn serde_round_trip_subscribe() {
122        let cmd = DebugCommand::Subscribe {
123            session_id: "s1".into(),
124            mode: SubscriptionMode::Live,
125            from_index: Some(10),
126        };
127        let json = serde_json::to_string(&cmd).unwrap();
128        assert!(json.contains("\"type\":\"subscribe\""));
129        let back: DebugCommand = serde_json::from_str(&json).unwrap();
130        match back {
131            DebugCommand::Subscribe {
132                session_id,
133                mode,
134                from_index,
135            } => {
136                assert_eq!(session_id, "s1");
137                assert_eq!(mode, SubscriptionMode::Live);
138                assert_eq!(from_index, Some(10));
139            }
140            _ => panic!("wrong variant"),
141        }
142    }
143
144    #[test]
145    fn serde_round_trip_list_sessions() {
146        let cmd = DebugCommand::ListSessions {
147            limit: None,
148            active_only: Some(true),
149        };
150        let json = serde_json::to_string(&cmd).unwrap();
151        assert!(json.contains("\"type\":\"listSessions\""));
152        let back: DebugCommand = serde_json::from_str(&json).unwrap();
153        match back {
154            DebugCommand::ListSessions { limit, active_only } => {
155                assert!(limit.is_none());
156                assert_eq!(active_only, Some(true));
157            }
158            _ => panic!("wrong variant"),
159        }
160    }
161
162    #[test]
163    fn serde_breakpoint_config() {
164        let bp = BreakpointConfig {
165            id: "bp1".into(),
166            bp_type: BreakpointType::TransitionStart,
167            target: Some("t1".into()),
168            enabled: true,
169        };
170        let json = serde_json::to_string(&bp).unwrap();
171        assert!(json.contains("\"type\":\"TRANSITION_START\""));
172        let back: BreakpointConfig = serde_json::from_str(&json).unwrap();
173        assert_eq!(back.bp_type, BreakpointType::TransitionStart);
174    }
175
176    #[test]
177    fn serde_event_filter_all() {
178        let filter = EventFilter::all();
179        let json = serde_json::to_string(&filter).unwrap();
180        let back: EventFilter = serde_json::from_str(&json).unwrap();
181        assert!(back.event_types.is_none());
182        assert!(back.transition_names.is_none());
183        assert!(back.place_names.is_none());
184    }
185
186    #[test]
187    fn serde_all_command_variants() {
188        let cmds = vec![
189            DebugCommand::ListSessions {
190                limit: Some(10),
191                active_only: None,
192            },
193            DebugCommand::Subscribe {
194                session_id: "s1".into(),
195                mode: SubscriptionMode::Replay,
196                from_index: None,
197            },
198            DebugCommand::Unsubscribe {
199                session_id: "s1".into(),
200            },
201            DebugCommand::Seek {
202                session_id: "s1".into(),
203                timestamp: "2025-01-01T00:00:00Z".into(),
204            },
205            DebugCommand::PlaybackSpeed {
206                session_id: "s1".into(),
207                speed: 2.0,
208            },
209            DebugCommand::Filter {
210                session_id: "s1".into(),
211                filter: EventFilter::all(),
212            },
213            DebugCommand::Pause {
214                session_id: "s1".into(),
215            },
216            DebugCommand::Resume {
217                session_id: "s1".into(),
218            },
219            DebugCommand::StepForward {
220                session_id: "s1".into(),
221            },
222            DebugCommand::StepBackward {
223                session_id: "s1".into(),
224            },
225            DebugCommand::SetBreakpoint {
226                session_id: "s1".into(),
227                breakpoint: BreakpointConfig {
228                    id: "bp1".into(),
229                    bp_type: BreakpointType::TokenAdded,
230                    target: None,
231                    enabled: true,
232                },
233            },
234            DebugCommand::ClearBreakpoint {
235                session_id: "s1".into(),
236                breakpoint_id: "bp1".into(),
237            },
238            DebugCommand::ListBreakpoints {
239                session_id: "s1".into(),
240            },
241            DebugCommand::ListArchives {
242                limit: None,
243                prefix: None,
244            },
245            DebugCommand::ImportArchive {
246                session_id: "s1".into(),
247            },
248            DebugCommand::UploadArchive {
249                file_name: "test.gz".into(),
250                data: "base64data".into(),
251            },
252        ];
253        for cmd in cmds {
254            let json = serde_json::to_string(&cmd).unwrap();
255            let _back: DebugCommand = serde_json::from_str(&json).unwrap();
256        }
257    }
258}