use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ExecutionControl {
Break,
Resume,
StepOver,
StepInto,
StepOut,
Terminate,
Abort,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "command", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Command {
VersionString,
Login {
user_name: String,
#[serde(default)]
password: String,
},
Logout,
LoadFile {
path: String,
},
Start {
#[serde(default = "main_sequence")]
sequence: String,
},
Run {
sequence_file: String,
#[serde(default = "main_sequence")]
sequence: String,
},
Terminate {
execution_id: i32,
},
Control {
#[serde(default, skip_serializing_if = "Option::is_none")]
execution_id: Option<i32>,
control: ExecutionControl,
},
ReadValue {
execution_id: i32,
lookup: String,
},
Shutdown,
}
fn main_sequence() -> String {
"MainSequence".to_owned()
}
impl Command {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::VersionString => "version_string",
Self::Login { .. } => "login",
Self::Logout => "logout",
Self::LoadFile { .. } => "load_file",
Self::Start { .. } => "start",
Self::Run { .. } => "run",
Self::Terminate { .. } => "terminate",
Self::Control { .. } => "control",
Self::ReadValue { .. } => "read_value",
Self::Shutdown => "shutdown",
}
}
#[must_use]
pub const fn is_control(&self) -> bool {
matches!(
self,
Self::Run { .. }
| Self::Start { .. }
| Self::Terminate { .. }
| Self::Login { .. }
| Self::Logout
| Self::Shutdown
)
}
}
#[cfg(test)]
mod tests {
#[test]
fn a_control_command_names_its_verb_on_the_wire() {
let text = serde_json::to_string(&Command::Control {
execution_id: Some(4),
control: ExecutionControl::StepOver,
})
.unwrap_or_default();
assert_eq!(
text,
r#"{"command":"control","execution_id":4,"control":"step_over"}"#
);
}
#[test]
fn a_control_without_an_execution_omits_the_field() {
let text = serde_json::to_string(&Command::Control {
execution_id: None,
control: ExecutionControl::Resume,
})
.unwrap_or_default();
assert_eq!(text, r#"{"command":"control","control":"resume"}"#);
}
#[test]
fn every_control_verb_round_trips() {
for control in [
ExecutionControl::Break,
ExecutionControl::Resume,
ExecutionControl::StepOver,
ExecutionControl::StepInto,
ExecutionControl::StepOut,
ExecutionControl::Terminate,
ExecutionControl::Abort,
] {
let command = Command::Control {
execution_id: None,
control,
};
let text = serde_json::to_string(&command).unwrap_or_default();
let back: Command = serde_json::from_str(&text).unwrap_or(Command::Shutdown);
assert_eq!(back, command, "{text}");
}
}
use super::{Command, ExecutionControl};
#[test]
fn the_wire_form_is_tagged_by_a_command_field() {
let text = serde_json::to_string(&Command::VersionString).unwrap_or_default();
assert_eq!(text, r#"{"command":"version_string"}"#);
}
#[test]
fn a_run_may_omit_the_sequence_name() {
let parsed: Command =
serde_json::from_str(r#"{"command":"run","sequence_file":"C:\\x.seq"}"#)
.unwrap_or(Command::Shutdown);
assert_eq!(
parsed,
Command::Run {
sequence_file: r"C:\x.seq".to_owned(),
sequence: "MainSequence".to_owned(),
}
);
}
#[test]
fn commands_round_trip() {
for command in [
Command::VersionString,
Command::Terminate { execution_id: 1 },
Command::ReadValue {
execution_id: 1,
lookup: "Locals.Result".to_owned(),
},
Command::Shutdown,
] {
let text = serde_json::to_string(&command).unwrap_or_default();
let back: Command = serde_json::from_str(&text).unwrap_or(Command::VersionString);
assert_eq!(back, command, "round trip failed for {}", command.name());
}
}
#[test]
fn reads_are_not_control_but_runs_are() {
assert!(!Command::VersionString.is_control());
assert!(
!Command::ReadValue {
execution_id: 1,
lookup: "Locals".to_owned()
}
.is_control()
);
assert!(Command::Terminate { execution_id: 1 }.is_control());
assert!(Command::Shutdown.is_control());
}
}