1use crate::manifest::GgenManifest;
2use ggen_utils::error::Result;
3use std::collections::{HashMap, HashSet};
4use std::path::Path;
5
6pub struct DependencyValidator;
7
8pub struct DependencyCheck {
9 pub name: String,
10 pub version: String,
11 pub passed: bool,
12 pub details: String,
13}
14
15pub struct DependencyValidationReport {
16 pub manifest_path: String,
17 pub total_checks: usize,
18 pub passed_checks: usize,
19 pub failed_checks: usize,
20 pub checks: Vec<DependencyCheck>,
21 pub has_cycles: bool,
22 pub cycle_nodes: Vec<String>,
23}
24
25impl DependencyValidator {
26 pub fn validate_manifest(
27 manifest: &GgenManifest, base_path: &Path,
28 ) -> Result<DependencyValidationReport> {
29 let mut checks = vec![];
30 let mut failed_count = 0;
31
32 let ontology_path = base_path.join(&manifest.ontology.source);
34 let ontology_check = if ontology_path.exists() {
35 DependencyCheck {
36 name: "ontology_exists".to_string(),
37 version: "1.0.0".to_string(),
38 passed: true,
39 details: format!("Ontology found at {}", ontology_path.display()),
40 }
41 } else {
42 failed_count += 1;
43 DependencyCheck {
44 name: "ontology_exists".to_string(),
45 version: "1.0.0".to_string(),
46 passed: false,
47 details: format!("Ontology not found at {}", ontology_path.display()),
48 }
49 };
50 checks.push(ontology_check);
51
52 for import in &manifest.ontology.imports {
54 let import_path = base_path.join(import);
55 let import_name = import.display().to_string();
56 let import_check = if import_path.exists() {
57 DependencyCheck {
58 name: format!("import_{}", import_name),
59 version: "1.0.0".to_string(),
60 passed: true,
61 details: format!("Import found at {}", import_path.display()),
62 }
63 } else {
64 failed_count += 1;
65 DependencyCheck {
66 name: format!("import_{}", import_name),
67 version: "1.0.0".to_string(),
68 passed: false,
69 details: format!("Import not found at {}", import_path.display()),
70 }
71 };
72 checks.push(import_check);
73 }
74
75 let (has_cycles, cycle_nodes) = Self::detect_inference_cycles(&manifest.inference.rules);
77 if has_cycles {
78 failed_count += 1;
79 checks.push(DependencyCheck {
80 name: "inference_cycles".to_string(),
81 version: "1.0.0".to_string(),
82 passed: false,
83 details: format!("Circular dependency detected: {:?}", cycle_nodes),
84 });
85 } else {
86 checks.push(DependencyCheck {
87 name: "inference_cycles".to_string(),
88 version: "1.0.0".to_string(),
89 passed: true,
90 details: "No circular dependencies detected".to_string(),
91 });
92 }
93
94 for rule in &manifest.generation.rules {
96 if let crate::manifest::TemplateSource::File { file } = &rule.template {
97 let template_path = base_path.join(file);
98 let template_check = if template_path.exists() {
99 DependencyCheck {
100 name: format!("template_{}", rule.name),
101 version: "1.0.0".to_string(),
102 passed: true,
103 details: format!("Template found at {}", template_path.display()),
104 }
105 } else {
106 failed_count += 1;
107 DependencyCheck {
108 name: format!("template_{}", rule.name),
109 version: "1.0.0".to_string(),
110 passed: false,
111 details: format!("Template not found at {}", template_path.display()),
112 }
113 };
114 checks.push(template_check);
115 }
116 }
117
118 for rule in &manifest.generation.rules {
120 if let crate::manifest::QuerySource::File { file } = &rule.query {
121 let query_path = base_path.join(file);
122 let query_check = if query_path.exists() {
123 DependencyCheck {
124 name: format!("query_{}", rule.name),
125 version: "1.0.0".to_string(),
126 passed: true,
127 details: format!("Query found at {}", query_path.display()),
128 }
129 } else {
130 failed_count += 1;
131 DependencyCheck {
132 name: format!("query_{}", rule.name),
133 version: "1.0.0".to_string(),
134 passed: false,
135 details: format!("Query not found at {}", query_path.display()),
136 }
137 };
138 checks.push(query_check);
139 }
140 }
141
142 let total_checks = checks.len();
143 let passed_checks = total_checks - failed_count;
144
145 Ok(DependencyValidationReport {
146 manifest_path: manifest.project.name.clone(),
147 total_checks,
148 passed_checks,
149 failed_checks: failed_count,
150 checks,
151 has_cycles,
152 cycle_nodes,
153 })
154 }
155
156 fn detect_inference_cycles(rules: &[crate::manifest::InferenceRule]) -> (bool, Vec<String>) {
157 let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
158 let mut visited: HashSet<String> = HashSet::new();
159 let mut rec_stack: HashSet<String> = HashSet::new();
160 let mut cycles = vec![];
161
162 for rule in rules {
163 graph.insert(rule.name.clone(), HashSet::new());
164 }
165
166 for rule in rules {
168 if let Some(when) = &rule.when {
169 for other_rule in rules {
170 if when.contains(&other_rule.name) && other_rule.name != rule.name {
171 if let Some(deps) = graph.get_mut(&rule.name) {
172 deps.insert(other_rule.name.clone());
173 }
174 }
175 }
176 }
177 }
178
179 for node in graph.keys() {
180 if !visited.contains(node) {
181 let has_cycle =
182 Self::dfs_cycle(&graph, node, &mut visited, &mut rec_stack, &mut cycles);
183 if has_cycle {
184 return (true, cycles);
185 }
186 }
187 }
188
189 (false, vec![])
190 }
191
192 fn dfs_cycle(
193 graph: &HashMap<String, HashSet<String>>, node: &str, visited: &mut HashSet<String>,
194 rec_stack: &mut HashSet<String>, cycles: &mut Vec<String>,
195 ) -> bool {
196 visited.insert(node.to_string());
197 rec_stack.insert(node.to_string());
198
199 if let Some(neighbors) = graph.get(node) {
200 for neighbor in neighbors {
201 if !visited.contains(neighbor) {
202 if Self::dfs_cycle(graph, neighbor, visited, rec_stack, cycles) {
203 return true;
204 }
205 } else if rec_stack.contains(neighbor) {
206 cycles.push(format!("{} -> {}", node, neighbor));
207 return true;
208 }
209 }
210 }
211
212 rec_stack.remove(node);
213 false
214 }
215}
216
217impl Default for crate::manifest::GenerationRule {
218 fn default() -> Self {
219 Self {
220 name: String::new(),
221 query: crate::manifest::QuerySource::Inline {
222 inline: String::new(),
223 },
224 template: crate::manifest::TemplateSource::Inline {
225 inline: String::new(),
226 },
227 output_file: String::new(),
228 skip_empty: false,
229 mode: crate::manifest::GenerationMode::Overwrite,
230 when: None,
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_cycle_detection() {
241 let rules = vec![
242 crate::manifest::InferenceRule {
243 name: "rule1".to_string(),
244 construct: "".to_string(),
245 order: 0,
246 description: None,
247 when: Some("rule2".to_string()),
248 },
249 crate::manifest::InferenceRule {
250 name: "rule2".to_string(),
251 construct: "".to_string(),
252 order: 1,
253 description: None,
254 when: Some("rule1".to_string()),
255 },
256 ];
257
258 let (has_cycle, cycles) = DependencyValidator::detect_inference_cycles(&rules);
259 assert!(has_cycle);
260 assert!(!cycles.is_empty());
261 }
262}