1use crate::interrupt::ResumeValue;
2use crate::state::State;
3
4#[derive(Clone, Debug)]
8pub struct Command<S: State> {
9 pub update: Option<S::Update>,
11
12 pub goto: Goto,
14
15 pub graph: GraphTarget,
17
18 pub resume: Option<ResumeValue>,
24
25 pub stream_data: Vec<serde_json::Value>,
32}
33
34#[derive(Clone, Debug)]
36pub enum Goto {
37 None,
39
40 Next(String),
42
43 Multiple(Vec<String>),
45
46 Send(Vec<SendTarget>),
48
49 End,
51}
52
53#[derive(Clone, Debug)]
55pub struct SendTarget {
56 pub node: String,
58
59 pub state: serde_json::Value,
61
62 pub timeout: Option<std::time::Duration>,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
68pub enum GraphTarget {
69 Current,
71
72 Parent,
74}
75
76#[derive(Debug)]
81pub struct Final<V, S> {
82 pub value: V,
84
85 pub save: S,
87}
88
89#[derive(Clone, Debug)]
94pub enum CommandGoto {
95 One(String),
97
98 Many(Vec<String>),
100
101 Parent(String),
103
104 Send(Vec<SendTarget>),
106}
107
108#[derive(Clone)]
124pub struct ParentCommand<S: State> {
125 pub command: Command<S>,
127
128 pub source_node: String,
130
131 pub namespace: String,
133}
134
135impl<S: State> ParentCommand<S> {
136 #[must_use]
144 pub fn from_subgraph(command: Command<S>, source_node: &str, namespace: &str) -> Self {
145 Self {
146 command,
147 source_node: source_node.to_string(),
148 namespace: namespace.to_string(),
149 }
150 }
151}
152
153impl<S: State> std::fmt::Debug for ParentCommand<S> {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.debug_struct("ParentCommand")
156 .field("command", &"<command>")
157 .field("source_node", &self.source_node)
158 .field("namespace", &self.namespace)
159 .finish()
160 }
161}
162
163impl<S: State> Command<S> {
164 #[must_use]
166 pub const fn update(update: S::Update) -> Self {
167 Self {
168 update: Some(update),
169 goto: Goto::None,
170 graph: GraphTarget::Current,
171 resume: None,
172 stream_data: Vec::new(),
173 }
174 }
175
176 #[must_use]
178 pub fn goto(target: impl Into<String>) -> Self {
179 Self {
180 update: None,
181 goto: Goto::Next(target.into()),
182 graph: GraphTarget::Current,
183 resume: None,
184 stream_data: Vec::new(),
185 }
186 }
187
188 #[must_use]
190 pub fn update_and_goto(update: S::Update, target: impl Into<String>) -> Self {
191 Self {
192 update: Some(update),
193 goto: Goto::Next(target.into()),
194 graph: GraphTarget::Current,
195 resume: None,
196 stream_data: Vec::new(),
197 }
198 }
199
200 #[must_use]
202 pub const fn send(targets: Vec<SendTarget>) -> Self {
203 Self {
204 update: None,
205 goto: Goto::Send(targets),
206 graph: GraphTarget::Current,
207 resume: None,
208 stream_data: Vec::new(),
209 }
210 }
211
212 #[must_use]
214 pub const fn update_and_send(update: S::Update, targets: Vec<SendTarget>) -> Self {
215 Self {
216 update: Some(update),
217 goto: Goto::Send(targets),
218 graph: GraphTarget::Current,
219 resume: None,
220 stream_data: Vec::new(),
221 }
222 }
223
224 #[must_use]
226 pub const fn end() -> Self {
227 Self {
228 update: None,
229 goto: Goto::End,
230 graph: GraphTarget::Current,
231 resume: None,
232 stream_data: Vec::new(),
233 }
234 }
235
236 pub fn goto_parent(target: impl Into<String>) -> Self {
238 Self {
239 update: None,
240 goto: Goto::Next(target.into()),
241 graph: GraphTarget::Parent,
242 resume: None,
243 stream_data: Vec::new(),
244 }
245 }
246
247 #[must_use]
249 pub fn with_resume(mut self, value: ResumeValue) -> Self {
250 self.resume = Some(value);
251 self
252 }
253
254 #[must_use]
271 pub fn with_stream_data(mut self, data: serde_json::Value) -> Self {
272 self.stream_data.push(data);
273 self
274 }
275}
276
277impl<S: State> Default for Command<S> {
278 fn default() -> Self {
279 Self {
280 update: None,
281 goto: Goto::None,
282 graph: GraphTarget::Current,
283 resume: None,
284 stream_data: Vec::new(),
285 }
286 }
287}
288
289#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::state::FieldVersions;
295 use serde_json::json;
296
297 #[derive(Clone, Debug, Default)]
298 struct TestState;
299
300 impl State for TestState {
301 type Update = TestUpdate;
302 type FieldVersions = FieldVersions;
303 fn apply(&mut self, _: Self::Update) -> crate::FieldsChanged {
304 crate::FieldsChanged(0)
305 }
306 fn reset_ephemeral(&mut self) {}
307 }
308
309 #[derive(Clone, Debug, Default, serde::Serialize)]
310 struct TestUpdate;
311
312 #[test]
313 fn command_default_has_empty_stream_data() {
314 let cmd = Command::<TestState>::default();
315 assert!(cmd.stream_data.is_empty());
316 }
317
318 #[test]
319 fn command_update_has_empty_stream_data() {
320 let cmd = Command::<TestState>::update(TestUpdate);
321 assert!(cmd.stream_data.is_empty());
322 }
323
324 #[test]
325 fn command_goto_has_empty_stream_data() {
326 let cmd = Command::<TestState>::goto("target");
327 assert!(cmd.stream_data.is_empty());
328 }
329
330 #[test]
331 fn command_end_has_empty_stream_data() {
332 let cmd = Command::<TestState>::end();
333 assert!(cmd.stream_data.is_empty());
334 }
335
336 #[test]
337 fn command_with_stream_data_appends_single_item() {
338 let cmd = Command::<TestState>::end().with_stream_data(json!({"key": "value"}));
339 assert_eq!(cmd.stream_data.len(), 1);
340 assert_eq!(cmd.stream_data[0], json!({"key": "value"}));
341 }
342
343 #[test]
344 fn command_with_stream_data_appends_multiple_items() {
345 let cmd = Command::<TestState>::end()
346 .with_stream_data(json!({"step": 1}))
347 .with_stream_data(json!({"step": 2}))
348 .with_stream_data(json!({"step": 3}));
349 assert_eq!(cmd.stream_data.len(), 3);
350 assert_eq!(cmd.stream_data[0], json!({"step": 1}));
351 assert_eq!(cmd.stream_data[1], json!({"step": 2}));
352 assert_eq!(cmd.stream_data[2], json!({"step": 3}));
353 }
354
355 #[test]
356 fn command_with_stream_data_preserves_other_fields() {
357 let cmd = Command::<TestState>::update(TestUpdate)
358 .with_stream_data(json!("progress"))
359 .with_resume(ResumeValue::Single(json!("resumed")));
360 assert!(cmd.update.is_some());
361 assert_eq!(cmd.stream_data.len(), 1);
362 assert!(cmd.resume.is_some());
363 }
364
365 #[test]
366 fn command_with_stream_data_works_with_goto() {
367 let cmd = Command::<TestState>::goto("next_node").with_stream_data(json!("data"));
368 assert!(matches!(cmd.goto, Goto::Next(ref target) if target == "next_node"));
369 assert_eq!(cmd.stream_data.len(), 1);
370 }
371
372 #[test]
373 fn command_send_has_empty_stream_data() {
374 let cmd = Command::<TestState>::send(vec![]);
375 assert!(cmd.stream_data.is_empty());
376 }
377
378 #[test]
379 fn command_goto_parent_has_empty_stream_data() {
380 let cmd = Command::<TestState>::goto_parent("parent");
381 assert!(cmd.stream_data.is_empty());
382 }
383}