ggen_core/codegen/
dependency_validation.rs

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        // Check 1: Ontology exists and is readable
33        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        // Check 2: All ontology imports exist
53        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        // Check 3: Inference rule ordering (topological sort for dependencies)
76        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        // Check 4: Generation rule templates exist (if file-based)
95        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        // Check 5: Query files exist (if file-based)
119        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        // Build dependency graph based on rule names
167        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}