1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub enum SubscriptionMode {
9 Live,
10 Replay,
11}
12
13#[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#[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#[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 pub fn all() -> Self {
54 Self::default()
55 }
56}
57
58#[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}