1use crate::ast::*;
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet};
7
8#[derive(Debug, Serialize, Deserialize)]
9pub struct ValidationResult {
10 pub valid: bool,
11 pub errors: Vec<Diagnostic>,
12 pub fixable: bool,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub fixed_source: Option<String>,
15}
16
17pub fn validate(doc: &Document, parse_diagnostics: &[Diagnostic]) -> (ValidationResult, Document) {
20 let mut diagnostics: Vec<Diagnostic> = parse_diagnostics.to_vec();
21 let mut resolved = doc.clone();
22
23 let mut seen_ids: HashMap<&str, &Node> = HashMap::new();
25 for node in &doc.nodes {
26 if let Some(existing) = seen_ids.get(node.id.as_str()) {
27 diagnostics.push(Diagnostic {
28 line: node.span.line,
29 col: node.span.col,
30 code: "E010".into(),
31 message: format!("Duplicate node ID '{}' (first declared at line {})", node.id, existing.span.line),
32 suggestion: Some(format!("Rename to '{}_2' or use a unique identifier", node.id)),
33 severity: Severity::Error,
34 });
35 } else {
36 seen_ids.insert(&node.id, node);
37 }
38 }
39
40 let declared: HashSet<String> = doc.nodes.iter().map(|n| n.id.clone()).collect();
42 let mut auto_added: HashSet<String> = HashSet::new();
43
44 for conn in &doc.connections {
45 for ref_id in [&conn.from, &conn.to] {
46 if !declared.contains(ref_id.as_str()) && !auto_added.contains(ref_id.as_str()) {
47 diagnostics.push(Diagnostic {
48 line: conn.span.line,
49 col: conn.span.col,
50 code: "W010".into(),
51 message: format!("Node '{}' used but not declared, treating as service", ref_id),
52 suggestion: Some(format!("Add: service {}", ref_id)),
53 severity: Severity::Warning,
54 });
55 resolved.nodes.push(Node {
57 node_type: NodeType::Service,
58 id: ref_id.clone(),
59 label: None,
60 tags: Vec::new(),
61 span: conn.span.clone(),
62 });
63 auto_added.insert(ref_id.clone());
64 }
65 }
66 }
67
68 fn check_group_refs(
70 group: &Group,
71 declared: &HashSet<String>,
72 auto_added: &mut HashSet<String>,
73 diagnostics: &mut Vec<Diagnostic>,
74 resolved_nodes: &mut Vec<Node>,
75 ) {
76 for member in &group.members {
77 match member {
78 GroupMember::NodeRef(id) => {
79 if !declared.contains(id) && !auto_added.contains(id) {
80 diagnostics.push(Diagnostic {
81 line: group.span.line,
82 col: group.span.col,
83 code: "W011".into(),
84 message: format!("Node '{}' referenced in group '{}' but not declared", id, group.label),
85 suggestion: Some(format!("Add: service {}", id)),
86 severity: Severity::Warning,
87 });
88 resolved_nodes.push(Node {
89 node_type: NodeType::Service,
90 id: id.clone(),
91 label: None,
92 tags: Vec::new(),
93 span: group.span.clone(),
94 });
95 auto_added.insert(id.clone());
96 }
97 }
98 GroupMember::NodeRefList(ids) => {
99 for id in ids {
100 if !declared.contains(id) && !auto_added.contains(id) {
101 diagnostics.push(Diagnostic {
102 line: group.span.line,
103 col: group.span.col,
104 code: "W011".into(),
105 message: format!("Node '{}' referenced in group '{}' but not declared", id, group.label),
106 suggestion: Some(format!("Add: service {}", id)),
107 severity: Severity::Warning,
108 });
109 resolved_nodes.push(Node {
110 node_type: NodeType::Service,
111 id: id.clone(),
112 label: None,
113 tags: Vec::new(),
114 span: group.span.clone(),
115 });
116 auto_added.insert(id.clone());
117 }
118 }
119 }
120 GroupMember::Group(g) => {
121 check_group_refs(g, declared, auto_added, diagnostics, resolved_nodes);
122 }
123 _ => {}
124 }
125 }
126 }
127
128 for group in &doc.groups {
129 check_group_refs(group, &declared, &mut auto_added, &mut diagnostics, &mut resolved.nodes);
130 }
131
132 let mut referenced: HashSet<&str> = HashSet::new();
134 for conn in &doc.connections {
135 referenced.insert(&conn.from);
136 referenced.insert(&conn.to);
137 }
138 fn collect_group_member_ids<'a>(group: &'a Group, ids: &mut HashSet<&'a str>) {
139 for member in &group.members {
140 match member {
141 GroupMember::NodeRef(id) => { ids.insert(id); }
142 GroupMember::NodeRefList(list) => { for id in list { ids.insert(id); } }
143 GroupMember::Node(n) => { ids.insert(&n.id); }
144 GroupMember::Connection(c) => { ids.insert(&c.from); ids.insert(&c.to); }
145 GroupMember::Group(g) => collect_group_member_ids(g, ids),
146 }
147 }
148 }
149 for group in &doc.groups {
150 collect_group_member_ids(group, &mut referenced);
151 }
152
153 for node in &doc.nodes {
154 if !referenced.contains(node.id.as_str()) {
155 diagnostics.push(Diagnostic {
156 line: node.span.line,
157 col: node.span.col,
158 code: "I001".into(),
159 message: format!("Node '{}' is declared but never used in connections or groups", node.id),
160 suggestion: None,
161 severity: Severity::Info,
162 });
163 }
164 }
165
166 for conn in &doc.connections {
168 if conn.from == conn.to {
169 diagnostics.push(Diagnostic {
170 line: conn.span.line,
171 col: conn.span.col,
172 code: "W012".into(),
173 message: format!("Self-connection on '{}' — is this intentional?", conn.from),
174 suggestion: None,
175 severity: Severity::Warning,
176 });
177 }
178 }
179
180 diagnostics.sort_by(|a, b| {
182 a.line.cmp(&b.line)
183 .then(severity_order(&a.severity).cmp(&severity_order(&b.severity)))
184 });
185
186 let has_errors = diagnostics.iter().any(|d| d.severity == Severity::Error);
187 let fixable = diagnostics.iter().all(|d| d.suggestion.is_some() || d.severity != Severity::Error);
188
189 let result = ValidationResult {
190 valid: !has_errors,
191 errors: diagnostics,
192 fixable,
193 fixed_source: None, };
195
196 (result, resolved)
197}
198
199fn severity_order(s: &Severity) -> u8 {
200 match s {
201 Severity::Error => 0,
202 Severity::Warning => 1,
203 Severity::Info => 2,
204 }
205}