1use thiserror::Error;
6
7use super::types::VerificationPointSpec;
8
9#[derive(Debug, Clone, Error)]
11pub enum VerificationError {
12 #[error("Module not found: {0}")]
13 ModuleNotFound(String),
14
15 #[error("Type not found: {0}")]
16 TypeNotFound(String),
17
18 #[error("Missing derive on {ty}: {derive}")]
19 MissingDerive { ty: String, derive: String },
20
21 #[error("Missing field on {ty}: {field}")]
22 MissingField { ty: String, field: String },
23
24 #[error("Method not found: {ty}::{method}")]
25 MethodNotFound { ty: String, method: String },
26
27 #[error("Compilation failed: {0}")]
28 CompilationFailed(String),
29
30 #[error("Warning detected: {0}")]
31 WarningDetected(String),
32}
33
34#[derive(Debug, Clone)]
36pub enum VerificationPoint {
37 ModuleExists(Vec<String>),
39
40 TypeExists(Vec<String>),
42
43 HasDerive { ty: String, derives: Vec<String> },
45
46 HasField {
48 ty: String,
49 field: String,
50 field_type: String,
51 },
52
53 MethodExists { ty: String, method: String },
55
56 Compiles,
58
59 NoWarnings { allowed: Vec<String> },
61}
62
63impl From<VerificationPointSpec> for VerificationPoint {
64 fn from(spec: VerificationPointSpec) -> Self {
65 match spec {
66 VerificationPointSpec::ModuleExists { paths } => VerificationPoint::ModuleExists(paths),
67 VerificationPointSpec::TypeExists { types } => VerificationPoint::TypeExists(types),
68 VerificationPointSpec::HasDerive { ty, derives } => {
69 VerificationPoint::HasDerive { ty, derives }
70 }
71 VerificationPointSpec::HasField {
72 ty,
73 field,
74 field_type,
75 } => VerificationPoint::HasField {
76 ty,
77 field,
78 field_type,
79 },
80 VerificationPointSpec::MethodExists { ty, method } => {
81 VerificationPoint::MethodExists { ty, method }
82 }
83 VerificationPointSpec::Compiles => VerificationPoint::Compiles,
84 VerificationPointSpec::NoWarnings { allowed } => {
85 VerificationPoint::NoWarnings { allowed }
86 }
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct VerificationResult {
94 pub phase: String,
96
97 pub total: usize,
99
100 pub passed: usize,
102
103 pub failures: Vec<(VerificationPoint, VerificationError)>,
105}
106
107impl VerificationResult {
108 pub fn all_passed(&self) -> bool {
110 self.failures.is_empty()
111 }
112}
113
114pub struct VerificationRunner {
116 project_root: std::path::PathBuf,
118}
119
120impl VerificationRunner {
121 pub fn new(project_root: impl Into<std::path::PathBuf>) -> Self {
123 Self {
124 project_root: project_root.into(),
125 }
126 }
127
128 pub fn run(&self, phase: &str, points: &[VerificationPoint]) -> VerificationResult {
130 let mut passed = 0;
131 let mut failures = Vec::new();
132
133 for point in points {
134 match self.verify_point(point) {
135 Ok(()) => passed += 1,
136 Err(e) => failures.push((point.clone(), e)),
137 }
138 }
139
140 VerificationResult {
141 phase: phase.to_string(),
142 total: points.len(),
143 passed,
144 failures,
145 }
146 }
147
148 fn verify_point(&self, point: &VerificationPoint) -> Result<(), VerificationError> {
150 match point {
151 VerificationPoint::ModuleExists(paths) => {
152 for path in paths {
153 self.check_module_exists(path)?;
154 }
155 Ok(())
156 }
157 VerificationPoint::TypeExists(types) => {
158 for ty in types {
159 self.check_type_exists(ty)?;
160 }
161 Ok(())
162 }
163 VerificationPoint::HasDerive { ty, derives } => {
164 for derive in derives {
165 self.check_has_derive(ty, derive)?;
166 }
167 Ok(())
168 }
169 VerificationPoint::HasField {
170 ty,
171 field,
172 field_type,
173 } => self.check_has_field(ty, field, field_type),
174 VerificationPoint::MethodExists { ty, method } => self.check_method_exists(ty, method),
175 VerificationPoint::Compiles => self.check_compiles(),
176 VerificationPoint::NoWarnings { allowed } => self.check_no_warnings(allowed),
177 }
178 }
179
180 fn check_module_exists(&self, path: &str) -> Result<(), VerificationError> {
182 let file_path = if path.contains("::") {
184 let parts: Vec<&str> = path.split("::").collect();
185 format!("src/{}.rs", parts.join("/"))
186 } else {
187 format!("src/{}.rs", path)
188 };
189
190 let full_path = self.project_root.join(&file_path);
191
192 let mod_path = if path.contains("::") {
194 let parts: Vec<&str> = path.split("::").collect();
195 format!("src/{}/mod.rs", parts.join("/"))
196 } else {
197 format!("src/{}/mod.rs", path)
198 };
199
200 let full_mod_path = self.project_root.join(&mod_path);
201
202 if full_path.exists() || full_mod_path.exists() {
203 Ok(())
204 } else {
205 Err(VerificationError::ModuleNotFound(path.to_string()))
206 }
207 }
208
209 fn check_type_exists(&self, ty: &str) -> Result<(), VerificationError> {
211 let src_path = self.project_root.join("src");
214
215 if !src_path.exists() {
216 return Err(VerificationError::TypeNotFound(ty.to_string()));
217 }
218
219 for entry in walkdir::WalkDir::new(&src_path)
222 .into_iter()
223 .filter_map(|e| e.ok())
224 .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
225 {
226 if let Ok(content) = std::fs::read_to_string(entry.path()) {
227 let patterns = [
229 format!("struct {} ", ty),
230 format!("struct {}(", ty),
231 format!("struct {} {{", ty),
232 format!("enum {} ", ty),
233 format!("enum {} {{", ty),
234 ];
235
236 for pattern in &patterns {
237 if content.contains(pattern) {
238 return Ok(());
239 }
240 }
241 }
242 }
243
244 Err(VerificationError::TypeNotFound(ty.to_string()))
245 }
246
247 fn check_has_derive(&self, ty: &str, derive: &str) -> Result<(), VerificationError> {
249 let src_path = self.project_root.join("src");
251
252 for entry in walkdir::WalkDir::new(&src_path)
253 .into_iter()
254 .filter_map(|e| e.ok())
255 .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
256 {
257 if let Ok(content) = std::fs::read_to_string(entry.path()) {
258 if let Some(pos) = content.find(&format!("struct {}", ty)) {
260 let before = &content[..pos];
261 if let Some(derive_pos) = before.rfind("#[derive(") {
262 let derive_block = &before[derive_pos..];
263 if derive_block.contains(derive) {
264 return Ok(());
265 }
266 }
267 }
268
269 if let Some(pos) = content.find(&format!("enum {}", ty)) {
270 let before = &content[..pos];
271 if let Some(derive_pos) = before.rfind("#[derive(") {
272 let derive_block = &before[derive_pos..];
273 if derive_block.contains(derive) {
274 return Ok(());
275 }
276 }
277 }
278 }
279 }
280
281 Err(VerificationError::MissingDerive {
282 ty: ty.to_string(),
283 derive: derive.to_string(),
284 })
285 }
286
287 fn check_has_field(
289 &self,
290 ty: &str,
291 field: &str,
292 _field_type: &str,
293 ) -> Result<(), VerificationError> {
294 let src_path = self.project_root.join("src");
296
297 for entry in walkdir::WalkDir::new(&src_path)
298 .into_iter()
299 .filter_map(|e| e.ok())
300 .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
301 {
302 if let Ok(content) = std::fs::read_to_string(entry.path()) {
303 if let Some(pos) = content.find(&format!("struct {} {{", ty)) {
305 if let Some(end) = content[pos..].find('}') {
307 let struct_body = &content[pos..pos + end];
308 if struct_body.contains(&field.to_string()) {
309 return Ok(());
310 }
311 }
312 }
313 }
314 }
315
316 Err(VerificationError::MissingField {
317 ty: ty.to_string(),
318 field: field.to_string(),
319 })
320 }
321
322 fn check_method_exists(&self, ty: &str, method: &str) -> Result<(), VerificationError> {
324 let src_path = self.project_root.join("src");
326
327 for entry in walkdir::WalkDir::new(&src_path)
328 .into_iter()
329 .filter_map(|e| e.ok())
330 .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
331 {
332 if let Ok(content) = std::fs::read_to_string(entry.path()) {
333 if content.contains(&format!("impl {}", ty))
335 && content.contains(&format!("fn {}(", method))
336 {
337 return Ok(());
338 }
339 }
340 }
341
342 Err(VerificationError::MethodNotFound {
343 ty: ty.to_string(),
344 method: method.to_string(),
345 })
346 }
347
348 fn check_compiles(&self) -> Result<(), VerificationError> {
350 let output = std::process::Command::new("cargo")
351 .arg("check")
352 .current_dir(&self.project_root)
353 .output();
354
355 match output {
356 Ok(out) if out.status.success() => Ok(()),
357 Ok(out) => {
358 let stderr = String::from_utf8_lossy(&out.stderr);
359 Err(VerificationError::CompilationFailed(stderr.to_string()))
360 }
361 Err(e) => Err(VerificationError::CompilationFailed(e.to_string())),
362 }
363 }
364
365 fn check_no_warnings(&self, allowed: &[String]) -> Result<(), VerificationError> {
367 let output = std::process::Command::new("cargo")
368 .args(["check", "--message-format=short"])
369 .current_dir(&self.project_root)
370 .output();
371
372 match output {
373 Ok(out) => {
374 let stderr = String::from_utf8_lossy(&out.stderr);
375
376 for line in stderr.lines() {
378 if line.contains("warning:") {
379 let is_allowed = allowed.iter().any(|a| line.contains(a));
380 if !is_allowed {
381 return Err(VerificationError::WarningDetected(line.to_string()));
382 }
383 }
384 }
385
386 Ok(())
387 }
388 Err(e) => Err(VerificationError::CompilationFailed(e.to_string())),
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_verification_result() {
399 let result = VerificationResult {
400 phase: "test".to_string(),
401 total: 3,
402 passed: 3,
403 failures: vec![],
404 };
405
406 assert!(result.all_passed());
407 }
408
409 #[test]
410 fn test_verification_point_from_spec() {
411 let spec = VerificationPointSpec::ModuleExists {
412 paths: vec!["user".to_string(), "product".to_string()],
413 };
414
415 let point: VerificationPoint = spec.into();
416
417 if let VerificationPoint::ModuleExists(paths) = point {
418 assert_eq!(paths.len(), 2);
419 } else {
420 panic!("Expected ModuleExists");
421 }
422 }
423}