smelt_validator/crucible/
adapter.rs1use crate::rules::ValidationRule;
4use crate::validator::{ValidationSeverity, Violation};
5use crucible_core::validator::{ValidationIssue, ValidationResult, Validator};
6use crucible_core::{Parser, Project};
7use smelt_core::{IntentRecord, SemanticDelta};
8use std::path::{Path, PathBuf};
9
10pub struct CrucibleAdapter {
12 project_root: PathBuf,
14 enabled: bool,
16 check_circular_deps: bool,
18 check_types: bool,
20 check_calls: bool,
22}
23
24impl CrucibleAdapter {
25 pub fn new(project_root: &Path) -> Self {
27 Self {
28 project_root: project_root.to_path_buf(),
29 enabled: true,
30 check_circular_deps: true,
31 check_types: true,
32 check_calls: true,
33 }
34 }
35
36 pub fn disabled() -> Self {
38 Self {
39 project_root: PathBuf::new(),
40 enabled: false,
41 check_circular_deps: false,
42 check_types: false,
43 check_calls: false,
44 }
45 }
46
47 pub fn with_circular_deps(mut self, check: bool) -> Self {
49 self.check_circular_deps = check;
50 self
51 }
52
53 pub fn with_type_checks(mut self, check: bool) -> Self {
55 self.check_types = check;
56 self
57 }
58
59 pub fn with_call_checks(mut self, check: bool) -> Self {
61 self.check_calls = check;
62 self
63 }
64
65 fn has_crucible_project(&self) -> bool {
67 self.project_root.join("crucible.json").exists()
68 || self.project_root.join("crucible.yaml").exists()
69 }
70
71 fn parse_project(&self) -> Option<Project> {
73 if !self.has_crucible_project() {
74 return None;
75 }
76
77 let parser = Parser::new(&self.project_root);
78 parser.parse_project().ok()
79 }
80
81 fn run_crucible_validation(&self) -> Vec<Violation> {
83 let Some(project) = self.parse_project() else {
84 return Vec::new();
85 };
86
87 let validator = Validator::new(project);
88 let result = validator.validate();
89
90 self.convert_result(&result)
91 }
92
93 fn convert_result(&self, result: &ValidationResult) -> Vec<Violation> {
95 let mut violations = Vec::new();
96
97 for issue in &result.errors {
99 if self.should_include_issue(issue) {
100 violations.push(self.convert_issue(issue, ValidationSeverity::Error));
101 }
102 }
103
104 for issue in &result.warnings {
106 if self.should_include_issue(issue) {
107 violations.push(self.convert_issue(issue, ValidationSeverity::Warning));
108 }
109 }
110
111 for issue in &result.info {
113 if self.should_include_issue(issue) {
114 violations.push(self.convert_issue(issue, ValidationSeverity::Info));
115 }
116 }
117
118 violations
119 }
120
121 fn should_include_issue(&self, issue: &ValidationIssue) -> bool {
123 match issue.rule.as_str() {
124 r if r.contains("circular") || r.contains("cycle") => self.check_circular_deps,
125 r if r.contains("type") => self.check_types,
126 r if r.contains("call") => self.check_calls,
127 _ => true,
128 }
129 }
130
131 fn convert_issue(&self, issue: &ValidationIssue, severity: ValidationSeverity) -> Violation {
133 let mut message = issue.message.clone();
134
135 if let (Some(found), Some(expected)) = (&issue.found, &issue.expected) {
137 message = format!("{} (found: {}, expected: {})", message, found, expected);
138 }
139
140 Violation {
141 rule: format!("crucible:{}", issue.rule),
142 severity,
143 message,
144 location: issue.location.clone(),
145 suggestion: issue.suggestion.clone(),
146 }
147 }
148}
149
150impl ValidationRule for CrucibleAdapter {
151 fn name(&self) -> &'static str {
152 "crucible"
153 }
154
155 fn validate(&self, _delta: &SemanticDelta, _intent: Option<&IntentRecord>) -> Vec<Violation> {
156 if !self.enabled {
157 return Vec::new();
158 }
159
160 self.run_crucible_validation()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use tempfile::tempdir;
168
169 #[test]
170 fn test_disabled_adapter() {
171 let adapter = CrucibleAdapter::disabled();
172 assert!(!adapter.enabled);
173 }
174
175 #[test]
176 fn test_no_crucible_project() {
177 let dir = tempdir().unwrap();
178 let adapter = CrucibleAdapter::new(dir.path());
179 assert!(!adapter.has_crucible_project());
180 }
181
182 #[test]
183 fn test_config_builder() {
184 let dir = tempdir().unwrap();
185 let adapter = CrucibleAdapter::new(dir.path())
186 .with_circular_deps(false)
187 .with_type_checks(false)
188 .with_call_checks(true);
189
190 assert!(!adapter.check_circular_deps);
191 assert!(!adapter.check_types);
192 assert!(adapter.check_calls);
193 }
194}