Skip to main content

a3s_flow/
result.rs

1//! [`FlowResult`] — the output of a completed flow execution.
2
3use std::collections::{HashMap, HashSet};
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use uuid::Uuid;
8
9/// Result of a complete or partial flow execution.
10///
11/// Returned by [`FlowRunner::run`](crate::runner::FlowRunner::run) and
12/// [`FlowRunner::resume_from`](crate::runner::FlowRunner::resume_from).
13/// Also stored by [`ExecutionStore`](crate::store::ExecutionStore) implementations.
14///
15/// `Serialize` / `Deserialize` are derived so that store implementations
16/// (e.g. SQLite, Redis) can round-trip the result as JSON without extra glue code.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct FlowResult {
19    /// Unique ID for this execution run.
20    pub execution_id: Uuid,
21    /// Per-node outputs, keyed by node ID.
22    ///
23    /// Skipped nodes (whose `run_if` evaluated to false) are present with a
24    /// `null` value; use `skipped_nodes` to distinguish them from nodes that
25    /// genuinely produced `null`.
26    pub outputs: HashMap<String, Value>,
27    /// IDs of all nodes that were processed (executed or skipped).
28    pub completed_nodes: HashSet<String>,
29    /// IDs of nodes whose `run_if` guard evaluated to false and were skipped.
30    pub skipped_nodes: HashSet<String>,
31    /// Final snapshot of the shared mutable context after execution.
32    ///
33    /// Nodes may read and write this map via [`ExecContext::context`](crate::node::ExecContext::context)
34    /// during execution. The final state is captured here when the flow completes.
35    #[serde(default)]
36    pub context: HashMap<String, Value>,
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42    use serde_json::json;
43
44    #[test]
45    fn serialize_deserialize_round_trip() {
46        let original = FlowResult {
47            execution_id: Uuid::new_v4(),
48            outputs: HashMap::from([
49                ("a".into(), json!({ "ok": true })),
50                ("b".into(), json!(null)),
51            ]),
52            completed_nodes: HashSet::from(["a".into(), "b".into()]),
53            skipped_nodes: HashSet::from(["b".into()]),
54            context: HashMap::from([("k".into(), json!("v"))]),
55        };
56
57        let json_str = serde_json::to_string(&original).expect("serialize");
58        let restored: FlowResult = serde_json::from_str(&json_str).expect("deserialize");
59
60        assert_eq!(restored.execution_id, original.execution_id);
61        assert_eq!(restored.outputs, original.outputs);
62        assert_eq!(restored.completed_nodes, original.completed_nodes);
63        assert_eq!(restored.skipped_nodes, original.skipped_nodes);
64        assert_eq!(restored.context, original.context);
65    }
66
67    #[test]
68    fn context_defaults_to_empty_on_deserialize() {
69        // Old serialized results without a "context" field should deserialize cleanly.
70        let json_str = r#"{"execution_id":"00000000-0000-0000-0000-000000000000","outputs":{},"completed_nodes":[],"skipped_nodes":[]}"#;
71        let result: FlowResult = serde_json::from_str(json_str).expect("deserialize");
72        assert!(result.context.is_empty());
73    }
74}