jellyflow_core/core/validate/
structural.rs1use 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
10pub 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}