1use crate::{AuditError, FeatureMention, Result, VersionReference, VersionType};
7use regex::Regex;
8use semver::Version;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::Path;
12use toml::Value;
13
14#[derive(Debug)]
16pub struct VersionValidator {
17 workspace_info: WorkspaceVersionInfo,
19 patterns: VersionPatterns,
21}
22
23#[derive(Debug, Clone)]
25pub struct WorkspaceVersionInfo {
26 pub workspace_version: String,
28 pub rust_version: String,
30 pub crate_versions: HashMap<String, String>,
32 pub dependency_versions: HashMap<String, String>,
34 pub workspace_features: HashMap<String, Vec<String>>,
36}
37
38#[derive(Debug)]
40struct VersionPatterns {
41 semver: Regex,
43 #[allow(dead_code)]
45 version_req: Regex,
46 #[allow(dead_code)]
48 git_version: Regex,
49 #[allow(dead_code)]
51 path_dependency: Regex,
52}
53
54#[derive(Debug, Clone, PartialEq)]
56pub struct VersionValidationResult {
57 pub is_valid: bool,
59 pub expected_version: Option<String>,
61 pub message: String,
63 pub severity: ValidationSeverity,
65 pub suggestion: Option<String>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum ValidationSeverity {
72 Critical,
74 Warning,
76 Info,
78}
79
80#[derive(Debug, Clone)]
82pub struct VersionValidationConfig {
83 pub allow_prerelease: bool,
85 pub strict_matching: bool,
87 pub validate_git_deps: bool,
89 pub version_tolerance: VersionTolerance,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum VersionTolerance {
96 Exact,
98 Patch,
100 Minor,
102 Major,
104}
105
106#[derive(Debug, Clone, Deserialize, Serialize)]
108#[serde(untagged)]
109pub enum DependencySpec {
110 Simple(String),
112 Detailed {
114 version: Option<String>,
115 git: Option<String>,
116 branch: Option<String>,
117 tag: Option<String>,
118 rev: Option<String>,
119 path: Option<String>,
120 features: Option<Vec<String>>,
121 #[serde(rename = "default-features")]
122 default_features: Option<bool>,
123 optional: Option<bool>,
124 },
125}
126
127impl VersionValidator {
128 pub async fn new(workspace_path: &Path) -> Result<Self> {
138 let workspace_info = Self::analyze_workspace(workspace_path).await?;
139 let patterns = VersionPatterns::new()?;
140
141 Ok(Self { workspace_info, patterns })
142 }
143
144 pub fn with_workspace_info(workspace_info: WorkspaceVersionInfo) -> Result<Self> {
148 let patterns = VersionPatterns::new()?;
149
150 Ok(Self { workspace_info, patterns })
151 }
152
153 pub fn validate_version_reference(
164 &self,
165 version_ref: &VersionReference,
166 config: &VersionValidationConfig,
167 ) -> Result<VersionValidationResult> {
168 match version_ref.version_type {
169 VersionType::RustVersion => self.validate_rust_version(version_ref, config),
170 VersionType::WorkspaceVersion => self.validate_workspace_version(version_ref, config),
171 VersionType::CrateVersion => self.validate_crate_version(version_ref, config),
172 VersionType::Generic => self.validate_generic_version(version_ref, config),
173 }
174 }
175
176 pub fn validate_version_references(
187 &self,
188 version_refs: &[VersionReference],
189 config: &VersionValidationConfig,
190 ) -> Result<Vec<VersionValidationResult>> {
191 version_refs
192 .iter()
193 .map(|version_ref| self.validate_version_reference(version_ref, config))
194 .collect()
195 }
196
197 pub fn validate_dependency_compatibility(
202 &self,
203 dependency_name: &str,
204 documented_version: &str,
205 config: &VersionValidationConfig,
206 ) -> Result<VersionValidationResult> {
207 if let Some(workspace_version) =
208 self.workspace_info.dependency_versions.get(dependency_name)
209 {
210 self.compare_versions(
211 documented_version,
212 workspace_version,
213 &format!("dependency '{}'", dependency_name),
214 config,
215 )
216 } else {
217 Ok(VersionValidationResult {
218 is_valid: false,
219 expected_version: None,
220 message: format!("Dependency '{}' not found in workspace", dependency_name),
221 severity: ValidationSeverity::Warning,
222 suggestion: Some(format!(
223 "Remove reference to '{}' or add it to workspace dependencies",
224 dependency_name
225 )),
226 })
227 }
228 }
229
230 pub fn is_version_compatible(
234 &self,
235 version1: &str,
236 version2: &str,
237 tolerance: &VersionTolerance,
238 ) -> Result<bool> {
239 let v1 = Version::parse(version1).map_err(|e| AuditError::ConfigurationError {
240 message: format!("Invalid version '{}': {}", version1, e),
241 })?;
242
243 let v2 = Version::parse(version2).map_err(|e| AuditError::ConfigurationError {
244 message: format!("Invalid version '{}': {}", version2, e),
245 })?;
246
247 let compatible = match tolerance {
248 VersionTolerance::Exact => v1 == v2,
249 VersionTolerance::Patch => v1.major == v2.major && v1.minor == v2.minor,
250 VersionTolerance::Minor => v1.major == v2.major,
251 VersionTolerance::Major => true, };
253
254 Ok(compatible)
255 }
256
257 pub fn validate_crate_exists(&self, crate_name: &str) -> VersionValidationResult {
267 if self.workspace_info.crate_versions.contains_key(crate_name) {
268 VersionValidationResult {
269 is_valid: true,
270 expected_version: None,
271 message: format!("Crate '{}' exists in workspace", crate_name),
272 severity: ValidationSeverity::Info,
273 suggestion: None,
274 }
275 } else {
276 let suggestion = self.suggest_similar_crate_name(crate_name);
277 VersionValidationResult {
278 is_valid: false,
279 expected_version: None,
280 message: format!("Crate '{}' not found in workspace", crate_name),
281 severity: ValidationSeverity::Warning,
282 suggestion,
283 }
284 }
285 }
286
287 pub fn validate_feature_flag(
298 &self,
299 crate_name: &str,
300 feature_name: &str,
301 ) -> VersionValidationResult {
302 if !self.workspace_info.crate_versions.contains_key(crate_name) {
304 return VersionValidationResult {
305 is_valid: false,
306 expected_version: None,
307 message: format!(
308 "Cannot validate feature '{}': crate '{}' not found in workspace",
309 feature_name, crate_name
310 ),
311 severity: ValidationSeverity::Warning,
312 suggestion: self.suggest_similar_crate_name(crate_name),
313 };
314 }
315
316 if let Some(features) = self.workspace_info.workspace_features.get(crate_name) {
318 if features.contains(&feature_name.to_string()) {
319 VersionValidationResult {
320 is_valid: true,
321 expected_version: None,
322 message: format!("Feature '{}' exists in crate '{}'", feature_name, crate_name),
323 severity: ValidationSeverity::Info,
324 suggestion: None,
325 }
326 } else {
327 let suggestion = self.suggest_similar_feature_name(crate_name, feature_name);
328 VersionValidationResult {
329 is_valid: false,
330 expected_version: None,
331 message: format!(
332 "Feature '{}' not found in crate '{}'",
333 feature_name, crate_name
334 ),
335 severity: ValidationSeverity::Warning,
336 suggestion,
337 }
338 }
339 } else {
340 VersionValidationResult {
341 is_valid: false,
342 expected_version: None,
343 message: format!("Crate '{}' has no features defined", crate_name),
344 severity: ValidationSeverity::Info,
345 suggestion: Some(format!("Check if '{}' is the correct crate name", crate_name)),
346 }
347 }
348 }
349
350 pub fn validate_crate_names(&self, crate_names: &[String]) -> Vec<VersionValidationResult> {
360 crate_names.iter().map(|crate_name| self.validate_crate_exists(crate_name)).collect()
361 }
362
363 pub fn validate_feature_mentions(
368 &self,
369 feature_mentions: &[FeatureMention],
370 ) -> Vec<VersionValidationResult> {
371 feature_mentions
372 .iter()
373 .map(|mention| {
374 if let Some(crate_name) = &mention.crate_name {
375 self.validate_feature_flag(crate_name, &mention.feature_name)
376 } else {
377 self.validate_feature_in_any_crate(&mention.feature_name)
379 }
380 })
381 .collect()
382 }
383
384 pub fn validate_feature_in_any_crate(&self, feature_name: &str) -> VersionValidationResult {
388 for (crate_name, features) in &self.workspace_info.workspace_features {
389 if features.contains(&feature_name.to_string()) {
390 return VersionValidationResult {
391 is_valid: true,
392 expected_version: None,
393 message: format!("Feature '{}' found in crate '{}'", feature_name, crate_name),
394 severity: ValidationSeverity::Info,
395 suggestion: None,
396 };
397 }
398 }
399
400 VersionValidationResult {
401 is_valid: false,
402 expected_version: None,
403 message: format!("Feature '{}' not found in any workspace crate", feature_name),
404 severity: ValidationSeverity::Warning,
405 suggestion: self.suggest_similar_feature_in_workspace(feature_name),
406 }
407 }
408
409 pub fn get_workspace_crates(&self) -> Vec<String> {
411 self.workspace_info.crate_versions.keys().cloned().collect()
412 }
413
414 pub fn get_crate_features(&self, crate_name: &str) -> Option<Vec<String>> {
416 self.workspace_info.workspace_features.get(crate_name).cloned()
417 }
418
419 pub fn get_all_workspace_features(&self) -> HashMap<String, Vec<String>> {
421 self.workspace_info.workspace_features.clone()
422 }
423
424 pub fn suggest_correct_version(&self, version_ref: &VersionReference) -> Option<String> {
429 match version_ref.version_type {
430 VersionType::RustVersion => Some(self.workspace_info.rust_version.clone()),
431 VersionType::WorkspaceVersion => Some(self.workspace_info.workspace_version.clone()),
432 VersionType::CrateVersion => {
433 if let Some(crate_name) = self.extract_crate_name_from_context(&version_ref.context)
435 {
436 self.workspace_info.crate_versions.get(&crate_name).cloned()
437 } else {
438 None
439 }
440 }
441 VersionType::Generic => {
442 self.find_best_version_match(&version_ref.version)
444 }
445 }
446 }
447
448 fn suggest_similar_crate_name(&self, target: &str) -> Option<String> {
450 let crate_names: Vec<&String> = self.workspace_info.crate_versions.keys().collect();
451
452 for crate_name in &crate_names {
454 if crate_name.contains(target) || target.contains(crate_name.as_str()) {
455 return Some(format!("Did you mean '{}'?", crate_name));
456 }
457 }
458
459 let mut best_match = None;
461 let mut best_distance = usize::MAX;
462
463 for crate_name in &crate_names {
464 let distance = self.edit_distance(target, crate_name);
465 if distance < best_distance && distance <= 3 {
466 best_distance = distance;
467 best_match = Some(crate_name);
468 }
469 }
470
471 if let Some(match_name) = best_match {
472 Some(format!("Did you mean '{}'?", match_name))
473 } else if !crate_names.is_empty() {
474 Some(format!(
475 "Available crates: {}",
476 crate_names.iter().take(5).map(|s| s.as_str()).collect::<Vec<_>>().join(", ")
477 ))
478 } else {
479 None
480 }
481 }
482
483 fn suggest_similar_feature_name(&self, crate_name: &str, target: &str) -> Option<String> {
485 if let Some(features) = self.workspace_info.workspace_features.get(crate_name) {
486 for feature in features {
488 if feature.contains(target) || target.contains(feature.as_str()) {
489 return Some(format!("Did you mean '{}'?", feature));
490 }
491 }
492
493 let mut best_match = None;
495 let mut best_distance = usize::MAX;
496
497 for feature in features {
498 let distance = self.edit_distance(target, feature);
499 if distance < best_distance && distance <= 2 {
500 best_distance = distance;
501 best_match = Some(feature);
502 }
503 }
504
505 if let Some(match_name) = best_match {
506 Some(format!("Did you mean '{}'?", match_name))
507 } else if !features.is_empty() {
508 Some(format!(
509 "Available features in '{}': {}",
510 crate_name,
511 features.iter().take(5).cloned().collect::<Vec<_>>().join(", ")
512 ))
513 } else {
514 None
515 }
516 } else {
517 None
518 }
519 }
520
521 fn suggest_similar_feature_in_workspace(&self, target: &str) -> Option<String> {
523 let mut all_features = Vec::new();
524
525 for (crate_name, features) in &self.workspace_info.workspace_features {
526 for feature in features {
527 all_features.push((crate_name, feature));
528 }
529 }
530
531 for (crate_name, feature) in &all_features {
533 if feature.contains(target) || target.contains(feature.as_str()) {
534 return Some(format!("Did you mean '{}' in crate '{}'?", feature, crate_name));
535 }
536 }
537
538 let mut best_match = None;
540 let mut best_distance = usize::MAX;
541
542 for (crate_name, feature) in &all_features {
543 let distance = self.edit_distance(target, feature);
544 if distance < best_distance && distance <= 2 {
545 best_distance = distance;
546 best_match = Some((crate_name, feature));
547 }
548 }
549
550 if let Some((crate_name, feature)) = best_match {
551 Some(format!("Did you mean '{}' in crate '{}'?", feature, crate_name))
552 } else if !all_features.is_empty() {
553 let sample_features: Vec<String> = all_features
554 .iter()
555 .take(3)
556 .map(|(crate_name, feature)| format!("'{}' in '{}'", feature, crate_name))
557 .collect();
558 Some(format!("Available features: {}", sample_features.join(", ")))
559 } else {
560 None
561 }
562 }
563
564 fn edit_distance(&self, s1: &str, s2: &str) -> usize {
566 let len1 = s1.len();
567 let len2 = s2.len();
568
569 if len1 == 0 {
570 return len2;
571 }
572 if len2 == 0 {
573 return len1;
574 }
575
576 let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
577
578 for (i, row) in matrix.iter_mut().enumerate().take(len1 + 1) {
580 row[0] = i;
581 }
582 #[allow(clippy::needless_range_loop)]
583 for j in 0..=len2 {
584 matrix[0][j] = j;
585 }
586
587 let s1_chars: Vec<char> = s1.chars().collect();
588 let s2_chars: Vec<char> = s2.chars().collect();
589
590 for i in 1..=len1 {
591 for j in 1..=len2 {
592 let cost = if s1_chars[i - 1] == s2_chars[j - 1] { 0 } else { 1 };
593
594 matrix[i][j] = std::cmp::min(
595 std::cmp::min(
596 matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, ),
599 matrix[i - 1][j - 1] + cost, );
601 }
602 }
603
604 matrix[len1][len2]
605 }
606
607 async fn analyze_workspace(workspace_path: &Path) -> Result<WorkspaceVersionInfo> {
609 let workspace_toml_path = workspace_path.join("Cargo.toml");
610 let workspace_content =
611 tokio::fs::read_to_string(&workspace_toml_path).await.map_err(|e| {
612 AuditError::IoError { path: workspace_toml_path.clone(), details: e.to_string() }
613 })?;
614
615 let workspace_toml: Value = toml::from_str(&workspace_content).map_err(|e| {
616 AuditError::TomlError { file_path: workspace_toml_path, details: e.to_string() }
617 })?;
618
619 let workspace_version = workspace_toml
621 .get("workspace")
622 .and_then(|w| w.get("package"))
623 .and_then(|p| p.get("version"))
624 .and_then(|v| v.as_str())
625 .unwrap_or("0.1.0")
626 .to_string();
627
628 let rust_version = workspace_toml
630 .get("workspace")
631 .and_then(|w| w.get("package"))
632 .and_then(|p| p.get("rust-version"))
633 .and_then(|v| v.as_str())
634 .unwrap_or("1.85.0")
635 .to_string();
636
637 let mut crate_versions = HashMap::new();
639 let mut dependency_versions = HashMap::new();
640 let mut workspace_features = HashMap::new();
641
642 if let Some(members) = workspace_toml
643 .get("workspace")
644 .and_then(|w| w.get("members"))
645 .and_then(|m| m.as_array())
646 {
647 for member in members {
648 if let Some(member_path) = member.as_str() {
649 let crate_path = workspace_path.join(member_path);
650 if let Ok(crate_info) = Self::analyze_crate(&crate_path).await {
651 crate_versions.insert(crate_info.name.clone(), crate_info.version);
652
653 for dep in crate_info.dependencies {
655 dependency_versions.insert(dep.name, dep.version);
656 }
657
658 if !crate_info.features.is_empty() {
660 workspace_features.insert(crate_info.name, crate_info.features);
661 }
662 }
663 }
664 }
665 }
666
667 Ok(WorkspaceVersionInfo {
668 workspace_version,
669 rust_version,
670 crate_versions,
671 dependency_versions,
672 workspace_features,
673 })
674 }
675
676 async fn analyze_crate(crate_path: &Path) -> Result<CrateAnalysisResult> {
678 let cargo_toml_path = crate_path.join("Cargo.toml");
679 let content = tokio::fs::read_to_string(&cargo_toml_path).await.map_err(|e| {
680 AuditError::IoError { path: cargo_toml_path.clone(), details: e.to_string() }
681 })?;
682
683 let cargo_toml: Value = toml::from_str(&content).map_err(|e| AuditError::TomlError {
684 file_path: cargo_toml_path,
685 details: e.to_string(),
686 })?;
687
688 let name = cargo_toml
690 .get("package")
691 .and_then(|p| p.get("name"))
692 .and_then(|n| n.as_str())
693 .unwrap_or("unknown")
694 .to_string();
695
696 let version = cargo_toml
697 .get("package")
698 .and_then(|p| p.get("version"))
699 .and_then(|v| v.as_str())
700 .unwrap_or("0.1.0")
701 .to_string();
702
703 let mut dependencies = Vec::new();
705 if let Some(deps) = cargo_toml.get("dependencies").and_then(|d| d.as_table()) {
706 for (dep_name, dep_spec) in deps {
707 if let Some(version) = Self::extract_dependency_version(dep_spec) {
708 dependencies.push(DependencyInfo { name: dep_name.clone(), version });
709 }
710 }
711 }
712
713 let mut features = Vec::new();
715 if let Some(feature_table) = cargo_toml.get("features").and_then(|f| f.as_table()) {
716 features.extend(feature_table.keys().cloned());
717 }
718
719 Ok(CrateAnalysisResult { name, version, dependencies, features })
720 }
721
722 fn extract_dependency_version(dep_spec: &Value) -> Option<String> {
724 match dep_spec {
725 Value::String(version) => Some(version.clone()),
726 Value::Table(table) => {
727 table.get("version").and_then(|v| v.as_str()).map(|s| s.to_string())
728 }
729 _ => None,
730 }
731 }
732
733 fn validate_rust_version(
735 &self,
736 version_ref: &VersionReference,
737 config: &VersionValidationConfig,
738 ) -> Result<VersionValidationResult> {
739 self.compare_versions(
740 &version_ref.version,
741 &self.workspace_info.rust_version,
742 "Rust version",
743 config,
744 )
745 }
746
747 fn validate_workspace_version(
749 &self,
750 version_ref: &VersionReference,
751 config: &VersionValidationConfig,
752 ) -> Result<VersionValidationResult> {
753 self.compare_versions(
754 &version_ref.version,
755 &self.workspace_info.workspace_version,
756 "workspace version",
757 config,
758 )
759 }
760
761 fn validate_crate_version(
763 &self,
764 version_ref: &VersionReference,
765 config: &VersionValidationConfig,
766 ) -> Result<VersionValidationResult> {
767 if let Some(crate_name) = self.extract_crate_name_from_context(&version_ref.context) {
769 if let Some(expected_version) = self.workspace_info.crate_versions.get(&crate_name) {
770 return self.compare_versions(
771 &version_ref.version,
772 expected_version,
773 &format!("crate '{}' version", crate_name),
774 config,
775 );
776 }
777 }
778
779 Ok(VersionValidationResult {
781 is_valid: true, expected_version: None,
783 message: "Unable to verify crate version - crate name not found in context".to_string(),
784 severity: ValidationSeverity::Info,
785 suggestion: Some(
786 "Ensure crate name is clearly specified in the documentation".to_string(),
787 ),
788 })
789 }
790
791 fn validate_generic_version(
793 &self,
794 version_ref: &VersionReference,
795 _config: &VersionValidationConfig,
796 ) -> Result<VersionValidationResult> {
797 if self.patterns.semver.is_match(&version_ref.version) {
799 Ok(VersionValidationResult {
800 is_valid: true,
801 expected_version: None,
802 message: "Version format is valid".to_string(),
803 severity: ValidationSeverity::Info,
804 suggestion: None,
805 })
806 } else {
807 Ok(VersionValidationResult {
808 is_valid: false,
809 expected_version: None,
810 message: format!("Invalid version format: '{}'", version_ref.version),
811 severity: ValidationSeverity::Warning,
812 suggestion: Some("Use semantic versioning format (e.g., '1.0.0')".to_string()),
813 })
814 }
815 }
816
817 fn compare_versions(
819 &self,
820 found_version: &str,
821 expected_version: &str,
822 context: &str,
823 config: &VersionValidationConfig,
824 ) -> Result<VersionValidationResult> {
825 if found_version == expected_version {
826 return Ok(VersionValidationResult {
827 is_valid: true,
828 expected_version: None,
829 message: format!("{} is correct", context),
830 severity: ValidationSeverity::Info,
831 suggestion: None,
832 });
833 }
834
835 let compatible =
837 self.is_version_compatible(found_version, expected_version, &config.version_tolerance)?;
838
839 if compatible {
840 Ok(VersionValidationResult {
841 is_valid: true,
842 expected_version: Some(expected_version.to_string()),
843 message: format!(
844 "{} '{}' is compatible with expected '{}'",
845 context, found_version, expected_version
846 ),
847 severity: ValidationSeverity::Info,
848 suggestion: None,
849 })
850 } else {
851 let severity = if config.strict_matching {
852 ValidationSeverity::Critical
853 } else {
854 ValidationSeverity::Warning
855 };
856
857 Ok(VersionValidationResult {
858 is_valid: false,
859 expected_version: Some(expected_version.to_string()),
860 message: format!(
861 "{} mismatch: found '{}', expected '{}'",
862 context, found_version, expected_version
863 ),
864 severity,
865 suggestion: Some(format!("Update to version '{}'", expected_version)),
866 })
867 }
868 }
869
870 fn extract_crate_name_from_context(&self, context: &str) -> Option<String> {
872 if let Some(captures) = Regex::new(r#"(adk[-_]\w+)"#).ok()?.captures(context) {
874 if let Some(crate_match) = captures.get(1) {
875 return Some(crate_match.as_str().replace('_', "-"));
876 }
877 }
878 None
879 }
880
881 fn find_best_version_match(&self, version: &str) -> Option<String> {
883 for dep_version in self.workspace_info.dependency_versions.values() {
885 if let (Ok(v1), Ok(v2)) = (Version::parse(version), Version::parse(dep_version)) {
886 if v1.major == v2.major {
887 return Some(dep_version.clone());
888 }
889 }
890 }
891 None
892 }
893}
894
895impl VersionPatterns {
896 fn new() -> Result<Self> {
898 Ok(Self {
899 semver: Regex::new(r"^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$")
900 .map_err(|e| AuditError::RegexError {
901 pattern: "semver".to_string(),
902 details: e.to_string(),
903 })?,
904
905 version_req: Regex::new(r"^[~^>=<]*\d+(?:\.\d+)?(?:\.\d+)?").map_err(|e| {
906 AuditError::RegexError {
907 pattern: "version_req".to_string(),
908 details: e.to_string(),
909 }
910 })?,
911
912 git_version: Regex::new(r#"git\s*=\s*"([^"]+)""#).map_err(|e| {
913 AuditError::RegexError {
914 pattern: "git_version".to_string(),
915 details: e.to_string(),
916 }
917 })?,
918
919 path_dependency: Regex::new(r#"path\s*=\s*"([^"]+)""#).map_err(|e| {
920 AuditError::RegexError {
921 pattern: "path_dependency".to_string(),
922 details: e.to_string(),
923 }
924 })?,
925 })
926 }
927}
928
929impl Default for VersionValidationConfig {
930 fn default() -> Self {
931 Self {
932 allow_prerelease: false,
933 strict_matching: false,
934 validate_git_deps: true,
935 version_tolerance: VersionTolerance::Patch,
936 }
937 }
938}
939
940#[derive(Debug, Clone)]
942struct CrateAnalysisResult {
943 name: String,
944 version: String,
945 dependencies: Vec<DependencyInfo>,
946 features: Vec<String>,
947}
948
949#[derive(Debug, Clone)]
951struct DependencyInfo {
952 name: String,
953 version: String,
954}
955
956#[cfg(test)]
957mod tests {
958 use super::*;
959 use std::collections::HashMap;
960
961 fn create_test_workspace_info() -> WorkspaceVersionInfo {
962 let mut crate_versions = HashMap::new();
963 crate_versions.insert("adk-core".to_string(), "0.1.0".to_string());
964 crate_versions.insert("adk-model".to_string(), "0.1.0".to_string());
965
966 let mut dependency_versions = HashMap::new();
967 dependency_versions.insert("serde".to_string(), "1.0.195".to_string());
968 dependency_versions.insert("tokio".to_string(), "1.35.0".to_string());
969
970 let mut workspace_features = HashMap::new();
971 workspace_features.insert("adk-core".to_string(), vec!["async".to_string()]);
972
973 WorkspaceVersionInfo {
974 workspace_version: "0.1.0".to_string(),
975 rust_version: "1.85.0".to_string(),
976 crate_versions,
977 dependency_versions,
978 workspace_features,
979 }
980 }
981
982 fn create_test_validator() -> VersionValidator {
983 let workspace_info = create_test_workspace_info();
984 VersionValidator::with_workspace_info(workspace_info).unwrap()
985 }
986
987 #[test]
988 fn test_validator_creation() {
989 let validator = create_test_validator();
990 assert_eq!(validator.workspace_info.workspace_version, "0.1.0");
991 assert_eq!(validator.workspace_info.rust_version, "1.85.0");
992 }
993
994 #[test]
995 fn test_rust_version_validation() {
996 let validator = create_test_validator();
997 let config = VersionValidationConfig::default();
998
999 let valid_ref = VersionReference {
1001 version: "1.85.0".to_string(),
1002 version_type: VersionType::RustVersion,
1003 line_number: 1,
1004 context: "rust-version = \"1.85.0\"".to_string(),
1005 };
1006
1007 let result = validator.validate_version_reference(&valid_ref, &config).unwrap();
1008 assert!(result.is_valid);
1009 assert_eq!(result.severity, ValidationSeverity::Info);
1010
1011 let invalid_ref = VersionReference {
1013 version: "1.80.0".to_string(),
1014 version_type: VersionType::RustVersion,
1015 line_number: 1,
1016 context: "rust-version = \"1.80.0\"".to_string(),
1017 };
1018
1019 let result = validator.validate_version_reference(&invalid_ref, &config).unwrap();
1020 assert!(!result.is_valid);
1021 assert_eq!(result.expected_version, Some("1.85.0".to_string()));
1022 }
1023
1024 #[test]
1025 fn test_workspace_version_validation() {
1026 let validator = create_test_validator();
1027 let config = VersionValidationConfig::default();
1028
1029 let version_ref = VersionReference {
1030 version: "0.1.0".to_string(),
1031 version_type: VersionType::WorkspaceVersion,
1032 line_number: 1,
1033 context: "adk-core = { version = \"0.1.0\" }".to_string(),
1034 };
1035
1036 let result = validator.validate_version_reference(&version_ref, &config).unwrap();
1037 assert!(result.is_valid);
1038 }
1039
1040 #[test]
1041 fn test_crate_version_validation() {
1042 let validator = create_test_validator();
1043 let config = VersionValidationConfig::default();
1044
1045 let version_ref = VersionReference {
1046 version: "0.1.0".to_string(),
1047 version_type: VersionType::CrateVersion,
1048 line_number: 1,
1049 context: "adk-core = { version = \"0.1.0\" }".to_string(),
1050 };
1051
1052 let result = validator.validate_version_reference(&version_ref, &config).unwrap();
1053 assert!(result.is_valid);
1054 }
1055
1056 #[test]
1057 fn test_version_compatibility() {
1058 let validator = create_test_validator();
1059
1060 assert!(
1062 validator.is_version_compatible("1.0.0", "1.0.0", &VersionTolerance::Exact).unwrap()
1063 );
1064 assert!(
1065 !validator.is_version_compatible("1.0.0", "1.0.1", &VersionTolerance::Exact).unwrap()
1066 );
1067
1068 assert!(
1070 validator.is_version_compatible("1.0.0", "1.0.1", &VersionTolerance::Patch).unwrap()
1071 );
1072 assert!(
1073 !validator.is_version_compatible("1.0.0", "1.1.0", &VersionTolerance::Patch).unwrap()
1074 );
1075
1076 assert!(
1078 validator.is_version_compatible("1.0.0", "1.1.0", &VersionTolerance::Minor).unwrap()
1079 );
1080 assert!(
1081 !validator.is_version_compatible("1.0.0", "2.0.0", &VersionTolerance::Minor).unwrap()
1082 );
1083
1084 assert!(
1086 validator.is_version_compatible("1.0.0", "2.0.0", &VersionTolerance::Major).unwrap()
1087 );
1088 }
1089
1090 #[test]
1091 fn test_dependency_compatibility() {
1092 let validator = create_test_validator();
1093 let config = VersionValidationConfig::default();
1094
1095 let result =
1097 validator.validate_dependency_compatibility("serde", "1.0.195", &config).unwrap();
1098 assert!(result.is_valid);
1099
1100 let result =
1102 validator.validate_dependency_compatibility("serde", "1.1.0", &config).unwrap();
1103 assert!(!result.is_valid);
1104 assert_eq!(result.expected_version, Some("1.0.195".to_string()));
1105
1106 let result =
1108 validator.validate_dependency_compatibility("unknown", "1.0.0", &config).unwrap();
1109 assert!(!result.is_valid);
1110 assert_eq!(result.severity, ValidationSeverity::Warning);
1111 }
1112
1113 #[test]
1114 fn test_version_suggestion() {
1115 let validator = create_test_validator();
1116
1117 let rust_ref = VersionReference {
1119 version: "1.80.0".to_string(),
1120 version_type: VersionType::RustVersion,
1121 line_number: 1,
1122 context: "rust-version = \"1.80.0\"".to_string(),
1123 };
1124 assert_eq!(validator.suggest_correct_version(&rust_ref), Some("1.85.0".to_string()));
1125
1126 let workspace_ref = VersionReference {
1128 version: "0.0.1".to_string(),
1129 version_type: VersionType::WorkspaceVersion,
1130 line_number: 1,
1131 context: "version = \"0.0.1\"".to_string(),
1132 };
1133 assert_eq!(validator.suggest_correct_version(&workspace_ref), Some("0.1.0".to_string()));
1134 }
1135
1136 #[test]
1137 fn test_crate_name_extraction() {
1138 let validator = create_test_validator();
1139
1140 assert_eq!(
1142 validator.extract_crate_name_from_context("adk-core = { version = \"0.1.0\" }"),
1143 Some("adk-core".to_string())
1144 );
1145
1146 assert_eq!(
1147 validator.extract_crate_name_from_context("use adk_core::Agent;"),
1148 Some("adk-core".to_string())
1149 );
1150
1151 assert_eq!(
1152 validator.extract_crate_name_from_context("The adk_model crate provides..."),
1153 Some("adk-model".to_string())
1154 );
1155
1156 assert_eq!(validator.extract_crate_name_from_context("No crate name here"), None);
1157 }
1158
1159 #[test]
1160 fn test_batch_validation() {
1161 let validator = create_test_validator();
1162 let config = VersionValidationConfig::default();
1163
1164 let version_refs = vec![
1165 VersionReference {
1166 version: "1.85.0".to_string(),
1167 version_type: VersionType::RustVersion,
1168 line_number: 1,
1169 context: "rust-version = \"1.85.0\"".to_string(),
1170 },
1171 VersionReference {
1172 version: "0.1.0".to_string(),
1173 version_type: VersionType::WorkspaceVersion,
1174 line_number: 2,
1175 context: "version = \"0.1.0\"".to_string(),
1176 },
1177 ];
1178
1179 let results = validator.validate_version_references(&version_refs, &config).unwrap();
1180 assert_eq!(results.len(), 2);
1181 assert!(results[0].is_valid);
1182 assert!(results[1].is_valid);
1183 }
1184
1185 #[test]
1186 fn test_crate_name_validation() {
1187 let validator = create_test_validator();
1188
1189 let result = validator.validate_crate_exists("adk-core");
1191 assert!(result.is_valid);
1192 assert_eq!(result.severity, ValidationSeverity::Info);
1193
1194 let result = validator.validate_crate_exists("nonexistent-crate");
1196 assert!(!result.is_valid);
1197 assert_eq!(result.severity, ValidationSeverity::Warning);
1198 assert!(result.suggestion.is_some());
1199 }
1200
1201 #[test]
1202 fn test_feature_flag_validation() {
1203 let validator = create_test_validator();
1204
1205 let result = validator.validate_feature_flag("adk-core", "async");
1207 assert!(result.is_valid);
1208 assert_eq!(result.severity, ValidationSeverity::Info);
1209
1210 let result = validator.validate_feature_flag("adk-core", "nonexistent-feature");
1212 assert!(!result.is_valid);
1213 assert_eq!(result.severity, ValidationSeverity::Warning);
1214
1215 let result = validator.validate_feature_flag("nonexistent-crate", "some-feature");
1217 assert!(!result.is_valid);
1218 assert_eq!(result.severity, ValidationSeverity::Warning);
1219 }
1220
1221 #[test]
1222 fn test_batch_crate_validation() {
1223 let validator = create_test_validator();
1224
1225 let crate_names =
1226 vec!["adk-core".to_string(), "adk-model".to_string(), "nonexistent".to_string()];
1227
1228 let results = validator.validate_crate_names(&crate_names);
1229 assert_eq!(results.len(), 3);
1230 assert!(results[0].is_valid); assert!(results[1].is_valid); assert!(!results[2].is_valid); }
1234
1235 #[test]
1236 fn test_feature_in_any_crate() {
1237 let validator = create_test_validator();
1238
1239 let result = validator.validate_feature_in_any_crate("async");
1241 assert!(result.is_valid);
1242
1243 let result = validator.validate_feature_in_any_crate("nonexistent-feature");
1245 assert!(!result.is_valid);
1246 }
1247
1248 #[test]
1249 fn test_workspace_queries() {
1250 let validator = create_test_validator();
1251
1252 let crates = validator.get_workspace_crates();
1254 assert!(crates.contains(&"adk-core".to_string()));
1255 assert!(crates.contains(&"adk-model".to_string()));
1256
1257 let features = validator.get_crate_features("adk-core");
1259 assert!(features.is_some());
1260 assert!(features.unwrap().contains(&"async".to_string()));
1261
1262 let all_features = validator.get_all_workspace_features();
1264 assert!(all_features.contains_key("adk-core"));
1265 }
1266
1267 #[test]
1268 fn test_similar_name_suggestions() {
1269 let validator = create_test_validator();
1270
1271 let result = validator.validate_crate_exists("adk-cor"); assert!(!result.is_valid);
1274 assert!(result.suggestion.is_some());
1275 assert!(result.suggestion.unwrap().contains("adk-core"));
1276
1277 let result = validator.validate_feature_flag("adk-core", "asyn"); assert!(!result.is_valid);
1280 assert!(result.suggestion.is_some());
1281 assert!(result.suggestion.unwrap().contains("async"));
1282 }
1283
1284 #[test]
1285 fn test_edit_distance() {
1286 let validator = create_test_validator();
1287
1288 assert_eq!(validator.edit_distance("", ""), 0);
1289 assert_eq!(validator.edit_distance("abc", "abc"), 0);
1290 assert_eq!(validator.edit_distance("abc", "ab"), 1);
1291 assert_eq!(validator.edit_distance("abc", "def"), 3);
1292 assert_eq!(validator.edit_distance("kitten", "sitting"), 3);
1293 }
1294
1295 #[test]
1296 fn test_version_tolerance_config() {
1297 let validator = create_test_validator();
1298
1299 let strict_config = VersionValidationConfig {
1301 strict_matching: true,
1302 version_tolerance: VersionTolerance::Exact,
1303 ..Default::default()
1304 };
1305
1306 let version_ref = VersionReference {
1307 version: "1.84.0".to_string(),
1308 version_type: VersionType::RustVersion,
1309 line_number: 1,
1310 context: "rust-version = \"1.84.0\"".to_string(),
1311 };
1312
1313 let result = validator.validate_version_reference(&version_ref, &strict_config).unwrap();
1314 assert!(!result.is_valid);
1315 assert_eq!(result.severity, ValidationSeverity::Critical);
1316
1317 let lenient_config = VersionValidationConfig {
1319 strict_matching: false,
1320 version_tolerance: VersionTolerance::Minor,
1321 ..Default::default()
1322 };
1323
1324 let major_diff_ref = VersionReference {
1326 version: "2.0.0".to_string(),
1327 version_type: VersionType::RustVersion,
1328 line_number: 1,
1329 context: "rust-version = \"2.0.0\"".to_string(),
1330 };
1331
1332 let result =
1333 validator.validate_version_reference(&major_diff_ref, &lenient_config).unwrap();
1334 assert!(!result.is_valid);
1336 assert_eq!(result.severity, ValidationSeverity::Warning);
1337 }
1338}