use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SubscriptionMode {
Live,
Replay,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum BreakpointType {
TransitionEnabled,
TransitionStart,
TransitionComplete,
TransitionFail,
TokenAdded,
TokenRemoved,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BreakpointConfig {
pub id: String,
#[serde(rename = "type")]
pub bp_type: BreakpointType,
pub target: Option<String>,
pub enabled: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EventFilter {
pub event_types: Option<Vec<String>>,
pub transition_names: Option<Vec<String>>,
pub place_names: Option<Vec<String>>,
#[serde(default)]
pub exclude_event_types: Option<Vec<String>>,
#[serde(default)]
pub exclude_transition_names: Option<Vec<String>>,
#[serde(default)]
pub exclude_place_names: Option<Vec<String>>,
}
impl EventFilter {
pub fn all() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum DebugCommand {
ListSessions {
limit: Option<usize>,
active_only: Option<bool>,
},
Subscribe {
session_id: String,
mode: SubscriptionMode,
from_index: Option<usize>,
},
Unsubscribe {
session_id: String,
},
Seek {
session_id: String,
timestamp: String,
},
PlaybackSpeed {
session_id: String,
speed: f64,
},
Filter {
session_id: String,
filter: EventFilter,
},
Pause {
session_id: String,
},
Resume {
session_id: String,
},
StepForward {
session_id: String,
},
StepBackward {
session_id: String,
},
SetBreakpoint {
session_id: String,
breakpoint: BreakpointConfig,
},
ClearBreakpoint {
session_id: String,
breakpoint_id: String,
},
ListBreakpoints {
session_id: String,
},
ListArchives {
limit: Option<usize>,
prefix: Option<String>,
},
ImportArchive {
session_id: String,
},
UploadArchive {
file_name: String,
data: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_round_trip_subscribe() {
let cmd = DebugCommand::Subscribe {
session_id: "s1".into(),
mode: SubscriptionMode::Live,
from_index: Some(10),
};
let json = serde_json::to_string(&cmd).unwrap();
assert!(json.contains("\"type\":\"subscribe\""));
let back: DebugCommand = serde_json::from_str(&json).unwrap();
match back {
DebugCommand::Subscribe {
session_id,
mode,
from_index,
} => {
assert_eq!(session_id, "s1");
assert_eq!(mode, SubscriptionMode::Live);
assert_eq!(from_index, Some(10));
}
_ => panic!("wrong variant"),
}
}
#[test]
fn serde_round_trip_list_sessions() {
let cmd = DebugCommand::ListSessions {
limit: None,
active_only: Some(true),
};
let json = serde_json::to_string(&cmd).unwrap();
assert!(json.contains("\"type\":\"listSessions\""));
let back: DebugCommand = serde_json::from_str(&json).unwrap();
match back {
DebugCommand::ListSessions { limit, active_only } => {
assert!(limit.is_none());
assert_eq!(active_only, Some(true));
}
_ => panic!("wrong variant"),
}
}
#[test]
fn serde_breakpoint_config() {
let bp = BreakpointConfig {
id: "bp1".into(),
bp_type: BreakpointType::TransitionStart,
target: Some("t1".into()),
enabled: true,
};
let json = serde_json::to_string(&bp).unwrap();
assert!(json.contains("\"type\":\"TRANSITION_START\""));
let back: BreakpointConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.bp_type, BreakpointType::TransitionStart);
}
#[test]
fn serde_event_filter_all() {
let filter = EventFilter::all();
let json = serde_json::to_string(&filter).unwrap();
let back: EventFilter = serde_json::from_str(&json).unwrap();
assert!(back.event_types.is_none());
assert!(back.transition_names.is_none());
assert!(back.place_names.is_none());
assert!(back.exclude_event_types.is_none());
assert!(back.exclude_transition_names.is_none());
assert!(back.exclude_place_names.is_none());
}
#[test]
fn serde_event_filter_backward_compat() {
let json = r#"{"eventTypes":["TransitionStarted"],"transitionNames":null,"placeNames":null}"#;
let filter: EventFilter = serde_json::from_str(json).unwrap();
assert!(filter.exclude_event_types.is_none());
assert!(filter.exclude_transition_names.is_none());
assert!(filter.exclude_place_names.is_none());
}
#[test]
fn serde_event_filter_with_exclusions() {
let filter = EventFilter {
event_types: None,
transition_names: None,
place_names: None,
exclude_event_types: Some(vec!["LogMessage".into()]),
exclude_transition_names: Some(vec!["t1".into()]),
exclude_place_names: None,
};
let json = serde_json::to_string(&filter).unwrap();
let back: EventFilter = serde_json::from_str(&json).unwrap();
assert_eq!(back.exclude_event_types, Some(vec!["LogMessage".into()]));
assert_eq!(back.exclude_transition_names, Some(vec!["t1".into()]));
assert!(back.exclude_place_names.is_none());
}
#[test]
fn serde_all_command_variants() {
let cmds = vec![
DebugCommand::ListSessions {
limit: Some(10),
active_only: None,
},
DebugCommand::Subscribe {
session_id: "s1".into(),
mode: SubscriptionMode::Replay,
from_index: None,
},
DebugCommand::Unsubscribe {
session_id: "s1".into(),
},
DebugCommand::Seek {
session_id: "s1".into(),
timestamp: "2025-01-01T00:00:00Z".into(),
},
DebugCommand::PlaybackSpeed {
session_id: "s1".into(),
speed: 2.0,
},
DebugCommand::Filter {
session_id: "s1".into(),
filter: EventFilter::all(),
},
DebugCommand::Pause {
session_id: "s1".into(),
},
DebugCommand::Resume {
session_id: "s1".into(),
},
DebugCommand::StepForward {
session_id: "s1".into(),
},
DebugCommand::StepBackward {
session_id: "s1".into(),
},
DebugCommand::SetBreakpoint {
session_id: "s1".into(),
breakpoint: BreakpointConfig {
id: "bp1".into(),
bp_type: BreakpointType::TokenAdded,
target: None,
enabled: true,
},
},
DebugCommand::ClearBreakpoint {
session_id: "s1".into(),
breakpoint_id: "bp1".into(),
},
DebugCommand::ListBreakpoints {
session_id: "s1".into(),
},
DebugCommand::ListArchives {
limit: None,
prefix: None,
},
DebugCommand::ImportArchive {
session_id: "s1".into(),
},
DebugCommand::UploadArchive {
file_name: "test.gz".into(),
data: "base64data".into(),
},
];
for cmd in cmds {
let json = serde_json::to_string(&cmd).unwrap();
let _back: DebugCommand = serde_json::from_str(&json).unwrap();
}
}
}