use crate::interrupt::ResumeValue;
use crate::state::State;
#[derive(Clone, Debug)]
pub struct Command<S: State> {
pub update: Option<S::Update>,
pub goto: Goto,
pub graph: GraphTarget,
pub resume: Option<ResumeValue>,
pub stream_data: Vec<serde_json::Value>,
}
#[derive(Clone, Debug)]
pub enum Goto {
None,
Next(String),
Multiple(Vec<String>),
Send(Vec<SendTarget>),
End,
}
#[derive(Clone, Debug)]
pub struct SendTarget {
pub node: String,
pub state: serde_json::Value,
pub timeout: Option<std::time::Duration>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GraphTarget {
Current,
Parent,
}
#[derive(Debug)]
pub struct Final<V, S> {
pub value: V,
pub save: S,
}
#[derive(Clone, Debug)]
pub enum CommandGoto {
One(String),
Many(Vec<String>),
Parent(String),
Send(Vec<SendTarget>),
}
#[derive(Clone)]
pub struct ParentCommand<S: State> {
pub command: Command<S>,
pub source_node: String,
pub namespace: String,
}
impl<S: State> ParentCommand<S> {
#[must_use]
pub fn from_subgraph(command: Command<S>, source_node: &str, namespace: &str) -> Self {
Self {
command,
source_node: source_node.to_string(),
namespace: namespace.to_string(),
}
}
}
impl<S: State> std::fmt::Debug for ParentCommand<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ParentCommand")
.field("command", &"<command>")
.field("source_node", &self.source_node)
.field("namespace", &self.namespace)
.finish()
}
}
impl<S: State> Command<S> {
#[must_use]
pub const fn update(update: S::Update) -> Self {
Self {
update: Some(update),
goto: Goto::None,
graph: GraphTarget::Current,
resume: None,
stream_data: Vec::new(),
}
}
#[must_use]
pub fn goto(target: impl Into<String>) -> Self {
Self {
update: None,
goto: Goto::Next(target.into()),
graph: GraphTarget::Current,
resume: None,
stream_data: Vec::new(),
}
}
#[must_use]
pub fn update_and_goto(update: S::Update, target: impl Into<String>) -> Self {
Self {
update: Some(update),
goto: Goto::Next(target.into()),
graph: GraphTarget::Current,
resume: None,
stream_data: Vec::new(),
}
}
#[must_use]
pub const fn send(targets: Vec<SendTarget>) -> Self {
Self {
update: None,
goto: Goto::Send(targets),
graph: GraphTarget::Current,
resume: None,
stream_data: Vec::new(),
}
}
#[must_use]
pub const fn update_and_send(update: S::Update, targets: Vec<SendTarget>) -> Self {
Self {
update: Some(update),
goto: Goto::Send(targets),
graph: GraphTarget::Current,
resume: None,
stream_data: Vec::new(),
}
}
#[must_use]
pub const fn end() -> Self {
Self {
update: None,
goto: Goto::End,
graph: GraphTarget::Current,
resume: None,
stream_data: Vec::new(),
}
}
pub fn goto_parent(target: impl Into<String>) -> Self {
Self {
update: None,
goto: Goto::Next(target.into()),
graph: GraphTarget::Parent,
resume: None,
stream_data: Vec::new(),
}
}
#[must_use]
pub fn with_resume(mut self, value: ResumeValue) -> Self {
self.resume = Some(value);
self
}
#[must_use]
pub fn with_stream_data(mut self, data: serde_json::Value) -> Self {
self.stream_data.push(data);
self
}
}
impl<S: State> Default for Command<S> {
fn default() -> Self {
Self {
update: None,
goto: Goto::None,
graph: GraphTarget::Current,
resume: None,
stream_data: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::FieldVersions;
use serde_json::json;
#[derive(Clone, Debug, Default)]
struct TestState;
impl State for TestState {
type Update = TestUpdate;
type FieldVersions = FieldVersions;
fn apply(&mut self, _: Self::Update) -> crate::FieldsChanged {
crate::FieldsChanged(0)
}
fn reset_ephemeral(&mut self) {}
}
#[derive(Clone, Debug, Default, serde::Serialize)]
struct TestUpdate;
#[test]
fn command_default_has_empty_stream_data() {
let cmd = Command::<TestState>::default();
assert!(cmd.stream_data.is_empty());
}
#[test]
fn command_update_has_empty_stream_data() {
let cmd = Command::<TestState>::update(TestUpdate);
assert!(cmd.stream_data.is_empty());
}
#[test]
fn command_goto_has_empty_stream_data() {
let cmd = Command::<TestState>::goto("target");
assert!(cmd.stream_data.is_empty());
}
#[test]
fn command_end_has_empty_stream_data() {
let cmd = Command::<TestState>::end();
assert!(cmd.stream_data.is_empty());
}
#[test]
fn command_with_stream_data_appends_single_item() {
let cmd = Command::<TestState>::end().with_stream_data(json!({"key": "value"}));
assert_eq!(cmd.stream_data.len(), 1);
assert_eq!(cmd.stream_data[0], json!({"key": "value"}));
}
#[test]
fn command_with_stream_data_appends_multiple_items() {
let cmd = Command::<TestState>::end()
.with_stream_data(json!({"step": 1}))
.with_stream_data(json!({"step": 2}))
.with_stream_data(json!({"step": 3}));
assert_eq!(cmd.stream_data.len(), 3);
assert_eq!(cmd.stream_data[0], json!({"step": 1}));
assert_eq!(cmd.stream_data[1], json!({"step": 2}));
assert_eq!(cmd.stream_data[2], json!({"step": 3}));
}
#[test]
fn command_with_stream_data_preserves_other_fields() {
let cmd = Command::<TestState>::update(TestUpdate)
.with_stream_data(json!("progress"))
.with_resume(ResumeValue::Single(json!("resumed")));
assert!(cmd.update.is_some());
assert_eq!(cmd.stream_data.len(), 1);
assert!(cmd.resume.is_some());
}
#[test]
fn command_with_stream_data_works_with_goto() {
let cmd = Command::<TestState>::goto("next_node").with_stream_data(json!("data"));
assert!(matches!(cmd.goto, Goto::Next(ref target) if target == "next_node"));
assert_eq!(cmd.stream_data.len(), 1);
}
#[test]
fn command_send_has_empty_stream_data() {
let cmd = Command::<TestState>::send(vec![]);
assert!(cmd.stream_data.is_empty());
}
#[test]
fn command_goto_parent_has_empty_stream_data() {
let cmd = Command::<TestState>::goto_parent("parent");
assert!(cmd.stream_data.is_empty());
}
}