Skip to main content

jellyflow_core/core/validate/
structural.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::core::{
4    EdgeId, EdgeKind, Graph, Node, NodeId, PortCapacity, PortId, PortKind,
5    subgraph_target_graph_id, symbol_ref_target_symbol_id,
6};
7
8use super::{GraphValidationError, GraphValidationReport, validate_graph_storage};
9
10/// Validates a graph for structural consistency (contract-level invariants).
11///
12/// This intentionally does **not** enforce editor policies such as connection direction.
13/// Direction, cycle policy, and domain-specific semantics belong in profiles/rules.
14pub fn validate_graph_structural(graph: &Graph) -> GraphValidationReport {
15    StructuralValidator::new(graph).finish()
16}
17
18struct StructuralValidator<'a> {
19    graph: &'a Graph,
20    report: GraphValidationReport,
21}
22
23impl<'a> StructuralValidator<'a> {
24    fn new(graph: &'a Graph) -> Self {
25        Self {
26            graph,
27            report: validate_graph_storage(graph),
28        }
29    }
30
31    fn finish(mut self) -> GraphValidationReport {
32        if self.report.has_unsupported_graph_version() {
33            return self.report;
34        }
35
36        self.validate_node_bindings();
37        self.validate_binding_relationships();
38        let incident_counts = self.validate_edges();
39        self.validate_port_capacities(incident_counts);
40        self.report
41    }
42
43    fn validate_node_bindings(&mut self) {
44        for (node_id, node) in &self.graph.nodes {
45            self.validate_subgraph_binding(*node_id, node);
46            self.validate_symbol_ref_binding(*node_id, node);
47        }
48    }
49
50    fn validate_subgraph_binding(&mut self, node_id: NodeId, node: &Node) {
51        match subgraph_target_graph_id(node_id, node) {
52            Ok(Some(target)) => {
53                if !self.graph.imports.contains_key(&target) {
54                    self.report
55                        .push(GraphValidationError::SubgraphTargetNotImported {
56                            node: node_id,
57                            graph_id: target,
58                        });
59                }
60            }
61            Ok(None) => {}
62            Err(err) => self.report.push(err.into()),
63        }
64    }
65
66    fn validate_symbol_ref_binding(&mut self, node_id: NodeId, node: &Node) {
67        match symbol_ref_target_symbol_id(node_id, node) {
68            Ok(Some(target)) => {
69                if !self.graph.symbols.contains_key(&target) {
70                    self.report
71                        .push(GraphValidationError::SymbolRefTargetNotDeclared {
72                            node: node_id,
73                            symbol_id: target,
74                        });
75                }
76            }
77            Ok(None) => {}
78            Err(err) => self.report.push(err.into()),
79        }
80    }
81
82    fn validate_binding_relationships(&mut self) {
83        for (binding_id, binding) in &self.graph.bindings {
84            for target in [
85                binding.subject.graph_local_target(),
86                binding.target.graph_local_target(),
87            ]
88            .into_iter()
89            .flatten()
90            {
91                if self.binding_target_exists(target) {
92                    continue;
93                }
94                self.report
95                    .push(GraphValidationError::BindingTargetMissing {
96                        binding: *binding_id,
97                        target,
98                    });
99            }
100        }
101    }
102
103    fn binding_target_exists(&self, target: crate::core::GraphLocalBindingTarget) -> bool {
104        match target {
105            crate::core::GraphLocalBindingTarget::Graph => true,
106            crate::core::GraphLocalBindingTarget::Node { id } => self.graph.nodes.contains_key(&id),
107            crate::core::GraphLocalBindingTarget::Port { id } => self.graph.ports.contains_key(&id),
108            crate::core::GraphLocalBindingTarget::Edge { id } => self.graph.edges.contains_key(&id),
109            crate::core::GraphLocalBindingTarget::Group { id } => {
110                self.graph.groups.contains_key(&id)
111            }
112            crate::core::GraphLocalBindingTarget::StickyNote { id } => {
113                self.graph.sticky_notes.contains_key(&id)
114            }
115        }
116    }
117
118    fn validate_edges(&mut self) -> BTreeMap<PortId, usize> {
119        let mut edges = EdgeValidationAccumulator::default();
120
121        for (edge_id, edge) in &self.graph.edges {
122            let Some(from) = self.graph.ports.get(&edge.from) else {
123                continue;
124            };
125            let Some(to) = self.graph.ports.get(&edge.to) else {
126                continue;
127            };
128
129            self.validate_edge_kind(*edge_id, from.kind, to.kind, edge.kind);
130
131            if edges.record(from.kind, edge.from, edge.to) {
132                self.report
133                    .push(GraphValidationError::DuplicateEdge { edge: *edge_id });
134            }
135        }
136
137        edges.into_incident_counts()
138    }
139
140    fn validate_edge_kind(
141        &mut self,
142        edge_id: EdgeId,
143        from_kind: PortKind,
144        to_kind: PortKind,
145        edge_kind: EdgeKind,
146    ) {
147        if from_kind != to_kind {
148            self.report.push(GraphValidationError::EdgeKindMismatch {
149                edge: edge_id,
150                from_kind,
151                to_kind,
152            });
153            return;
154        }
155
156        let expected = from_kind.edge_kind();
157        if edge_kind != expected {
158            self.report
159                .push(GraphValidationError::EdgeKindPortKindMismatch {
160                    edge: edge_id,
161                    edge_kind,
162                    port_kind: from_kind,
163                });
164        }
165    }
166
167    fn validate_port_capacities(&mut self, incident_counts: BTreeMap<PortId, usize>) {
168        for (port_id, count) in incident_counts {
169            let Some(port) = self.graph.ports.get(&port_id) else {
170                continue;
171            };
172            if port.capacity == PortCapacity::Single && count > 1 {
173                self.report
174                    .push(GraphValidationError::PortCapacityExceeded {
175                        port: port_id,
176                        capacity: port.capacity,
177                        count,
178                    });
179            }
180        }
181    }
182}
183
184#[derive(Default)]
185struct EdgeValidationAccumulator {
186    edge_pairs: BTreeSet<(PortKind, PortId, PortId)>,
187    incident_counts: BTreeMap<PortId, usize>,
188}
189
190impl EdgeValidationAccumulator {
191    fn record(&mut self, port_kind: PortKind, from: PortId, to: PortId) -> bool {
192        let is_duplicate = !self.edge_pairs.insert((port_kind, from, to));
193        *self.incident_counts.entry(from).or_insert(0) += 1;
194        *self.incident_counts.entry(to).or_insert(0) += 1;
195        is_duplicate
196    }
197
198    fn into_incident_counts(self) -> BTreeMap<PortId, usize> {
199        self.incident_counts
200    }
201}