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", rename_all_fields = "camelCase")]
61pub enum DebugCommand {
62 ListSessions {
63 limit: Option<usize>,
64 active_only: Option<bool>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
67 tag_filter: Option<std::collections::HashMap<String, String>>,
68 },
69 Subscribe {
70 session_id: String,
71 mode: SubscriptionMode,
72 from_index: Option<usize>,
73 },
74 Unsubscribe {
75 session_id: String,
76 },
77 Seek {
78 session_id: String,
79 timestamp: String,
80 },
81 PlaybackSpeed {
82 session_id: String,
83 speed: f64,
84 },
85 Filter {
86 session_id: String,
87 filter: EventFilter,
88 },
89 Pause {
90 session_id: String,
91 },
92 Resume {
93 session_id: String,
94 },
95 StepForward {
96 session_id: String,
97 },
98 StepBackward {
99 session_id: String,
100 },
101 SetBreakpoint {
102 session_id: String,
103 breakpoint: BreakpointConfig,
104 },
105 ClearBreakpoint {
106 session_id: String,
107 breakpoint_id: String,
108 },
109 ListBreakpoints {
110 session_id: String,
111 },
112 ListArchives {
113 limit: Option<usize>,
114 prefix: Option<String>,
115 },
116 ImportArchive {
117 session_id: String,
118 },
119 UploadArchive {
120 file_name: String,
121 data: String,
122 },
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn serde_round_trip_subscribe() {
131 let cmd = DebugCommand::Subscribe {
132 session_id: "s1".into(),
133 mode: SubscriptionMode::Live,
134 from_index: Some(10),
135 };
136 let json = serde_json::to_string(&cmd).unwrap();
137 assert!(json.contains("\"type\":\"subscribe\""));
138 let back: DebugCommand = serde_json::from_str(&json).unwrap();
139 match back {
140 DebugCommand::Subscribe {
141 session_id,
142 mode,
143 from_index,
144 } => {
145 assert_eq!(session_id, "s1");
146 assert_eq!(mode, SubscriptionMode::Live);
147 assert_eq!(from_index, Some(10));
148 }
149 _ => panic!("wrong variant"),
150 }
151 }
152
153 #[test]
154 fn serde_round_trip_list_sessions() {
155 let cmd = DebugCommand::ListSessions {
156 limit: None,
157 active_only: Some(true),
158 tag_filter: None,
159 };
160 let json = serde_json::to_string(&cmd).unwrap();
161 assert!(json.contains("\"type\":\"listSessions\""));
162 assert!(!json.contains("\"tagFilter\""));
164 let back: DebugCommand = serde_json::from_str(&json).unwrap();
165 match back {
166 DebugCommand::ListSessions {
167 limit,
168 active_only,
169 tag_filter,
170 } => {
171 assert!(limit.is_none());
172 assert_eq!(active_only, Some(true));
173 assert!(tag_filter.is_none());
174 }
175 _ => panic!("wrong variant"),
176 }
177 }
178
179 #[test]
180 fn serde_list_sessions_with_tag_filter() {
181 let mut filter = std::collections::HashMap::new();
182 filter.insert("channel".to_string(), "voice".to_string());
183 let cmd = DebugCommand::ListSessions {
184 limit: Some(10),
185 active_only: Some(false),
186 tag_filter: Some(filter),
187 };
188 let json = serde_json::to_string(&cmd).unwrap();
189 assert!(json.contains("\"tagFilter\""));
190 assert!(json.contains("\"activeOnly\""));
191 assert!(json.contains("\"channel\":\"voice\""));
192 let back: DebugCommand = serde_json::from_str(&json).unwrap();
193 if let DebugCommand::ListSessions { tag_filter, .. } = back {
194 let f = tag_filter.expect("tag_filter should be Some");
195 assert_eq!(f.get("channel"), Some(&"voice".to_string()));
196 } else {
197 panic!("wrong variant");
198 }
199 }
200
201 #[test]
202 fn serde_list_sessions_without_tag_filter() {
203 let json = r#"{"type":"listSessions","limit":10,"activeOnly":false}"#;
205 let cmd: DebugCommand = serde_json::from_str(json).unwrap();
206 if let DebugCommand::ListSessions {
207 limit,
208 active_only,
209 tag_filter,
210 } = cmd
211 {
212 assert_eq!(limit, Some(10));
213 assert_eq!(active_only, Some(false));
214 assert!(tag_filter.is_none());
215 } else {
216 panic!("wrong variant");
217 }
218 }
219
220 #[test]
221 fn serde_list_sessions_camelcase_interop() {
222 let json = r#"{"type":"listSessions","limit":10,"activeOnly":true,"tagFilter":{"channel":"voice","env":"staging"}}"#;
224 let cmd: DebugCommand = serde_json::from_str(json).unwrap();
225 if let DebugCommand::ListSessions {
226 limit,
227 active_only,
228 tag_filter,
229 } = cmd
230 {
231 assert_eq!(limit, Some(10));
232 assert_eq!(active_only, Some(true));
233 let f = tag_filter.expect("tag_filter should be Some");
234 assert_eq!(f.get("channel"), Some(&"voice".to_string()));
235 assert_eq!(f.get("env"), Some(&"staging".to_string()));
236 } else {
237 panic!("wrong variant");
238 }
239 }
240
241 #[test]
242 fn serde_subscribe_camelcase_interop() {
243 let json = r#"{"type":"subscribe","sessionId":"s1","mode":"live","fromIndex":10}"#;
245 let cmd: DebugCommand = serde_json::from_str(json).unwrap();
246 if let DebugCommand::Subscribe {
247 session_id,
248 mode,
249 from_index,
250 } = cmd
251 {
252 assert_eq!(session_id, "s1");
253 assert_eq!(mode, SubscriptionMode::Live);
254 assert_eq!(from_index, Some(10));
255 } else {
256 panic!("wrong variant");
257 }
258 }
259
260 #[test]
261 fn serde_breakpoint_config() {
262 let bp = BreakpointConfig {
263 id: "bp1".into(),
264 bp_type: BreakpointType::TransitionStart,
265 target: Some("t1".into()),
266 enabled: true,
267 };
268 let json = serde_json::to_string(&bp).unwrap();
269 assert!(json.contains("\"type\":\"TRANSITION_START\""));
270 let back: BreakpointConfig = serde_json::from_str(&json).unwrap();
271 assert_eq!(back.bp_type, BreakpointType::TransitionStart);
272 }
273
274 #[test]
275 fn serde_event_filter_all() {
276 let filter = EventFilter::all();
277 let json = serde_json::to_string(&filter).unwrap();
278 let back: EventFilter = serde_json::from_str(&json).unwrap();
279 assert!(back.event_types.is_none());
280 assert!(back.transition_names.is_none());
281 assert!(back.place_names.is_none());
282 assert!(back.exclude_event_types.is_none());
283 assert!(back.exclude_transition_names.is_none());
284 assert!(back.exclude_place_names.is_none());
285 }
286
287 #[test]
288 fn serde_event_filter_backward_compat() {
289 let json = r#"{"eventTypes":["TransitionStarted"],"transitionNames":null,"placeNames":null}"#;
290 let filter: EventFilter = serde_json::from_str(json).unwrap();
291 assert!(filter.exclude_event_types.is_none());
292 assert!(filter.exclude_transition_names.is_none());
293 assert!(filter.exclude_place_names.is_none());
294 }
295
296 #[test]
297 fn serde_event_filter_with_exclusions() {
298 let filter = EventFilter {
299 event_types: None,
300 transition_names: None,
301 place_names: None,
302 exclude_event_types: Some(vec!["LogMessage".into()]),
303 exclude_transition_names: Some(vec!["t1".into()]),
304 exclude_place_names: None,
305 };
306 let json = serde_json::to_string(&filter).unwrap();
307 let back: EventFilter = serde_json::from_str(&json).unwrap();
308 assert_eq!(back.exclude_event_types, Some(vec!["LogMessage".into()]));
309 assert_eq!(back.exclude_transition_names, Some(vec!["t1".into()]));
310 assert!(back.exclude_place_names.is_none());
311 }
312
313 #[test]
314 fn serde_all_command_variants() {
315 let cmds = vec![
316 DebugCommand::ListSessions {
317 limit: Some(10),
318 active_only: None,
319 tag_filter: None,
320 },
321 DebugCommand::Subscribe {
322 session_id: "s1".into(),
323 mode: SubscriptionMode::Replay,
324 from_index: None,
325 },
326 DebugCommand::Unsubscribe {
327 session_id: "s1".into(),
328 },
329 DebugCommand::Seek {
330 session_id: "s1".into(),
331 timestamp: "2025-01-01T00:00:00Z".into(),
332 },
333 DebugCommand::PlaybackSpeed {
334 session_id: "s1".into(),
335 speed: 2.0,
336 },
337 DebugCommand::Filter {
338 session_id: "s1".into(),
339 filter: EventFilter::all(),
340 },
341 DebugCommand::Pause {
342 session_id: "s1".into(),
343 },
344 DebugCommand::Resume {
345 session_id: "s1".into(),
346 },
347 DebugCommand::StepForward {
348 session_id: "s1".into(),
349 },
350 DebugCommand::StepBackward {
351 session_id: "s1".into(),
352 },
353 DebugCommand::SetBreakpoint {
354 session_id: "s1".into(),
355 breakpoint: BreakpointConfig {
356 id: "bp1".into(),
357 bp_type: BreakpointType::TokenAdded,
358 target: None,
359 enabled: true,
360 },
361 },
362 DebugCommand::ClearBreakpoint {
363 session_id: "s1".into(),
364 breakpoint_id: "bp1".into(),
365 },
366 DebugCommand::ListBreakpoints {
367 session_id: "s1".into(),
368 },
369 DebugCommand::ListArchives {
370 limit: None,
371 prefix: None,
372 },
373 DebugCommand::ImportArchive {
374 session_id: "s1".into(),
375 },
376 DebugCommand::UploadArchive {
377 file_name: "test.gz".into(),
378 data: "base64data".into(),
379 },
380 ];
381 for cmd in cmds {
382 let json = serde_json::to_string(&cmd).unwrap();
383 let _back: DebugCommand = serde_json::from_str(&json).unwrap();
384 }
385 }
386}