Skip to main content

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}