flow_domain/graph.rs
1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
4pub struct Position {
5 pub x: f64,
6 pub y: f64,
7}
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct FlowNode {
11 pub id: String,
12 /// JSON key is `type` (matches React Flow + the TS `FlowNodeDto`); the Rust
13 /// field is named `node_type` because `type` is a reserved keyword.
14 #[serde(rename = "type")]
15 pub node_type: String,
16 pub position: Position,
17 pub data: serde_json::Value,
18}
19
20/// Conditional routing outcome on an outgoing edge.
21///
22/// - `Pass` fires when the source node terminal status is `Succeeded`.
23/// - `Fail` fires when the source node terminal status is `Failed` or `Skipped`.
24/// - `Always` fires regardless of the source's outcome - the default for
25/// unconditional edges.
26#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
27#[serde(rename_all = "snake_case")]
28pub enum EdgeOutcome {
29 Pass,
30 Fail,
31 #[default]
32 Always,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct FlowEdge {
37 pub id: String,
38 pub source: String,
39 pub target: String,
40 pub label: Option<String>,
41 pub condition: Option<String>,
42 /// Conditional routing outcome. Defaults to `Always` so flows saved before
43 /// this field existed deserialize cleanly.
44 #[serde(default)]
45 pub outcome: EdgeOutcome,
46}
47
48/// A sub-flow: a named, first-class **execution unit** grouping a contiguous
49/// set of member nodes. The executor treats it as a single composite unit with
50/// one entry and one exit (derived from the edges crossing its boundary) and
51/// can retry the whole unit on failure. Membership is by node id; the nodes
52/// themselves stay in the flat `FlowGraph.nodes` list.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct SubFlow {
56 pub id: String,
57 pub label: String,
58 /// Ids of the member nodes (a subset of `FlowGraph.nodes`). Serializes as
59 /// `nodeIds` for the TS `SubFlowDto`.
60 pub node_ids: Vec<String>,
61 /// Re-run the whole unit up to this many times if it fails. `None` runs it
62 /// once (no retry).
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub retry: Option<u32>,
65}
66
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
68pub struct FlowGraph {
69 pub id: String,
70 pub name: String,
71 pub version: String,
72 /// Optional human-readable description of the workflow. Round-trips
73 /// through the DSL header `description "..."` line. `None` (or empty)
74 /// emits no header line so graphs without one serialise unchanged.
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub description: Option<String>,
77 pub nodes: Vec<FlowNode>,
78 pub edges: Vec<FlowEdge>,
79 /// Execution-unit groupings over `nodes`. `#[serde(default)]` so graphs
80 /// without sub-flows deserialize cleanly.
81 #[serde(default, skip_serializing_if = "Vec::is_empty")]
82 pub subflows: Vec<SubFlow>,
83}
84
85impl FlowGraph {
86 /// The sub-flow a node belongs to, if any.
87 pub fn subflow_of(&self, node_id: &str) -> Option<&SubFlow> {
88 self.subflows
89 .iter()
90 .find(|sf| sf.node_ids.iter().any(|id| id == node_id))
91 }
92}