featherbit/graph/
validation.rs1use std::collections::HashSet;
6
7use crate::config::PolicyConfig;
8
9pub fn validate_policy(policy: &PolicyConfig) -> Result<(), Vec<String>> {
23 let mut errors = Vec::new();
24
25 let node_ids: HashSet<&str> = policy.nodes.iter().map(|n| n.id.as_str()).collect();
26
27 let has_listener = policy.nodes.iter().any(|n| n.node_type == "listener");
29 if !has_listener {
30 errors.push("Policy must have a 'listener' node".to_string());
31 }
32
33 let has_client = policy.nodes.iter().any(|n| n.node_type == "client");
35 if !has_client {
36 errors.push("Policy must have a 'client' node".to_string());
37 }
38
39 for edge in &policy.edges {
41 let from_node = edge.from.split('.').next().unwrap_or("");
42 let to_node = edge.to.split('.').next().unwrap_or("");
43
44 if !node_ids.contains(from_node) {
45 errors.push(format!(
46 "Edge references unknown source node: '{}'",
47 from_node
48 ));
49 }
50 if !node_ids.contains(to_node) {
51 errors.push(format!(
52 "Edge references unknown target node: '{}'",
53 to_node
54 ));
55 }
56 }
57
58 let client_ids: HashSet<&str> = policy
62 .nodes
63 .iter()
64 .filter(|n| n.node_type == "client")
65 .map(|n| n.id.as_str())
66 .collect();
67 let error_handler_ids: HashSet<&str> = policy
68 .nodes
69 .iter()
70 .filter(|n| n.node_type == "error-handler")
71 .map(|n| n.id.as_str())
72 .collect();
73
74 let mut input_targets: HashSet<String> = HashSet::new();
75 for edge in &policy.edges {
76 let target = &edge.to;
77 let to_node = target.split('.').next().unwrap_or("");
78
79 let is_client = client_ids.contains(to_node);
80 let is_error_handler = error_handler_ids.contains(to_node);
81
82 if !is_client && !is_error_handler && !input_targets.insert(target.clone()) {
83 errors.push(format!(
84 "Node '{}' input '{}' has multiple incoming edges — each input accepts only one edge",
85 to_node, target
86 ));
87 }
88 }
89
90 let mut connected_nodes: HashSet<&str> = HashSet::new();
92 for edge in &policy.edges {
93 let from_node = edge.from.split('.').next().unwrap_or("");
94 let to_node = edge.to.split('.').next().unwrap_or("");
95 connected_nodes.insert(from_node);
96 connected_nodes.insert(to_node);
97 }
98
99 if let Some(ref handler) = policy.error_handler {
101 connected_nodes.insert(handler.as_str());
102 }
103
104 for node in &policy.nodes {
105 if !connected_nodes.contains(node.id.as_str()) {
106 errors.push(format!("Orphan node '{}' has no connections", node.id));
107 }
108 }
109
110 if let Some(ref handler) = policy.error_handler {
112 if !node_ids.contains(handler.as_str()) {
113 errors.push(format!(
114 "Policy error_handler references unknown node: '{}'",
115 handler
116 ));
117 }
118 }
119
120 if errors.is_empty() {
121 Ok(())
122 } else {
123 Err(errors)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::config::{EdgeConfig, NodeConfig, PolicyConfig};
131 use std::collections::HashMap;
132
133 fn listener_node() -> NodeConfig {
134 NodeConfig {
135 id: "listener".to_string(),
136 node_type: "listener".to_string(),
137 config: HashMap::new(),
138 position: None,
139 }
140 }
141
142 fn client_node() -> NodeConfig {
143 NodeConfig {
144 id: "client".to_string(),
145 node_type: "client".to_string(),
146 config: HashMap::new(),
147 position: None,
148 }
149 }
150
151 fn upstream_node() -> NodeConfig {
152 NodeConfig {
153 id: "backend".to_string(),
154 node_type: "upstream".to_string(),
155 config: HashMap::new(),
156 position: None,
157 }
158 }
159
160 #[test]
161 fn test_valid_simple_policy() {
162 let policy = PolicyConfig {
163 name: "test".to_string(),
164 error_handler: None,
165 nodes: vec![listener_node(), upstream_node(), client_node()],
166 edges: vec![
167 EdgeConfig {
168 from: "listener.out".to_string(),
169 to: "backend.in".to_string(),
170 },
171 EdgeConfig {
172 from: "backend.success".to_string(),
173 to: "client.in".to_string(),
174 },
175 ],
176 };
177 assert!(validate_policy(&policy).is_ok());
178 }
179
180 #[test]
181 fn test_missing_listener() {
182 let policy = PolicyConfig {
183 name: "test".to_string(),
184 error_handler: None,
185 nodes: vec![upstream_node(), client_node()],
186 edges: vec![],
187 };
188 let errors = validate_policy(&policy).unwrap_err();
189 assert!(errors.iter().any(|e| e.contains("listener")));
190 }
191
192 #[test]
193 fn test_missing_client() {
194 let policy = PolicyConfig {
195 name: "test".to_string(),
196 error_handler: None,
197 nodes: vec![listener_node(), upstream_node()],
198 edges: vec![EdgeConfig {
199 from: "listener.out".to_string(),
200 to: "backend.in".to_string(),
201 }],
202 };
203 let errors = validate_policy(&policy).unwrap_err();
204 assert!(errors.iter().any(|e| e.contains("client")));
205 }
206
207 #[test]
208 fn test_unknown_edge_reference() {
209 let policy = PolicyConfig {
210 name: "test".to_string(),
211 error_handler: None,
212 nodes: vec![listener_node(), client_node()],
213 edges: vec![EdgeConfig {
214 from: "listener.out".to_string(),
215 to: "nonexistent.in".to_string(),
216 }],
217 };
218 let errors = validate_policy(&policy).unwrap_err();
219 assert!(errors.iter().any(|e| e.contains("nonexistent")));
220 }
221
222 #[test]
223 fn test_client_allows_multiple_inputs() {
224 let policy = PolicyConfig {
225 name: "test".to_string(),
226 error_handler: None,
227 nodes: vec![listener_node(), upstream_node(), client_node()],
228 edges: vec![
229 EdgeConfig {
230 from: "listener.out".to_string(),
231 to: "backend.in".to_string(),
232 },
233 EdgeConfig {
234 from: "backend.success".to_string(),
235 to: "client.in".to_string(),
236 },
237 EdgeConfig {
238 from: "backend.error".to_string(),
239 to: "client.in".to_string(),
240 },
241 ],
242 };
243 assert!(validate_policy(&policy).is_ok());
244 }
245}