1use crate::goto::Goto;
18use crate::node::NodeOut;
19use crate::state::GraphState;
20
21#[derive(Debug, Clone)]
23pub struct Command<S: GraphState> {
24 update: S::Update,
25 goto: Goto,
26 payload: Option<serde_json::Value>,
27}
28
29impl<S: GraphState> Command<S> {
30 pub fn new(update: S::Update) -> Self {
32 Self {
33 update,
34 goto: Goto::End,
35 payload: None,
36 }
37 }
38
39 pub fn goto_only(goto: Goto) -> Self
41 where
42 S::Update: Default,
43 {
44 Self {
45 update: S::Update::default(),
46 goto,
47 payload: None,
48 }
49 }
50
51 pub fn goto(mut self, name: impl Into<String>) -> Self {
53 self.goto = Goto::Node(name.into());
54 self
55 }
56
57 pub fn goto_multiple<I, N>(mut self, names: I) -> Self
59 where
60 I: IntoIterator<Item = N>,
61 N: Into<String>,
62 {
63 self.goto = Goto::Multiple(names.into_iter().map(Into::into).collect());
64 self
65 }
66
67 pub fn send<I, N>(mut self, targets: I) -> Self
69 where
70 I: IntoIterator<Item = (N, serde_json::Value)>,
71 N: Into<String>,
72 {
73 self.goto = Goto::Send(targets.into_iter().map(|(n, p)| (n.into(), p)).collect());
74 self
75 }
76
77 pub fn end(mut self) -> Self {
79 self.goto = Goto::End;
80 self
81 }
82
83 pub fn with_payload(mut self, payload: serde_json::Value) -> Self {
87 match std::mem::replace(&mut self.goto, Goto::End) {
88 Goto::Node(name) => {
89 self.goto = Goto::Send(vec![(name, payload)]);
90 }
91 other @ Goto::End => {
92 self.goto = other;
93 }
94 other => {
95 self.goto = other;
98 self.payload = Some(payload);
99 }
100 }
101 self
102 }
103}
104
105impl<S: GraphState> From<Command<S>> for NodeOut<S> {
106 fn from(c: Command<S>) -> Self {
107 if c.payload.is_some() && !matches!(c.goto, Goto::End) {
110 tracing::debug!(
111 "Command::with_payload set on a non-Node goto; payload ignored \
112 (use Command::send for per-target payloads)"
113 );
114 }
115 NodeOut {
116 update: c.update,
117 goto: c.goto,
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[derive(Debug, Default, Clone, PartialEq)]
127 struct S {
128 n: u32,
129 }
130 #[derive(Debug, Default, Clone)]
131 struct SU {
132 n: u32,
133 }
134 impl GraphState for S {
135 type Update = SU;
136 fn apply(&mut self, u: Self::Update) {
137 self.n += u.n;
138 }
139 }
140
141 #[test]
142 fn defaults_to_end() {
143 let cmd: Command<S> = Command::new(SU { n: 1 });
144 let out: NodeOut<S> = cmd.into();
145 assert!(matches!(out.goto, Goto::End));
146 assert_eq!(out.update.n, 1);
147 }
148
149 #[test]
150 fn goto_routes_single() {
151 let cmd: Command<S> = Command::new(SU { n: 0 }).goto("next");
152 let out: NodeOut<S> = cmd.into();
153 assert_eq!(out.goto, Goto::Node("next".into()));
154 }
155
156 #[test]
157 fn with_payload_promotes_node_to_send() {
158 let cmd: Command<S> = Command::new(SU { n: 0 })
159 .goto("worker")
160 .with_payload(serde_json::json!({"x": 1}));
161 let out: NodeOut<S> = cmd.into();
162 if let Goto::Send(t) = out.goto {
163 assert_eq!(t.len(), 1);
164 assert_eq!(t[0].0, "worker");
165 assert_eq!(t[0].1["x"], 1);
166 } else {
167 panic!("expected Send");
168 }
169 }
170
171 #[test]
172 fn send_with_multiple_targets() {
173 let cmd: Command<S> = Command::new(SU { n: 0 }).send([
174 ("a", serde_json::json!({"i": 0})),
175 ("b", serde_json::json!({"i": 1})),
176 ]);
177 let out: NodeOut<S> = cmd.into();
178 if let Goto::Send(t) = out.goto {
179 assert_eq!(t.len(), 2);
180 } else {
181 panic!("expected Send");
182 }
183 }
184
185 #[test]
186 fn goto_only_skips_update() {
187 let cmd: Command<S> = Command::goto_only(Goto::node("x"));
188 let out: NodeOut<S> = cmd.into();
189 assert_eq!(out.goto, Goto::Node("x".into()));
190 assert_eq!(out.update.n, 0);
191 }
192}