ricecoder_orchestration/analyzers/
version_validator.rs

1//! Version constraint validation and compatibility checking
2
3use crate::error::{OrchestrationError, Result};
4
5/// Represents a semantic version
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub struct Version {
8    pub major: u32,
9    pub minor: u32,
10    pub patch: u32,
11}
12
13impl Version {
14    /// Parses a version string (e.g., "1.2.3")
15    pub fn parse(version_str: &str) -> Result<Self> {
16        let parts: Vec<&str> = version_str.trim_start_matches('v').split('.').collect();
17
18        if parts.len() < 3 {
19            return Err(OrchestrationError::VersionConstraintViolation(format!(
20                "Invalid version format: {}",
21                version_str
22            )));
23        }
24
25        let major = parts[0]
26            .parse::<u32>()
27            .map_err(|_| OrchestrationError::VersionConstraintViolation(format!(
28                "Invalid major version: {}",
29                parts[0]
30            )))?;
31
32        let minor = parts[1]
33            .parse::<u32>()
34            .map_err(|_| OrchestrationError::VersionConstraintViolation(format!(
35                "Invalid minor version: {}",
36                parts[1]
37            )))?;
38
39        let patch = parts[2]
40            .parse::<u32>()
41            .map_err(|_| OrchestrationError::VersionConstraintViolation(format!(
42                "Invalid patch version: {}",
43                parts[2]
44            )))?;
45
46        Ok(Version { major, minor, patch })
47    }
48
49    /// Converts version to string
50    pub fn version_string(&self) -> String {
51        format!("{}.{}.{}", self.major, self.minor, self.patch)
52    }
53}
54
55/// Represents a version constraint (e.g., "^1.2.3", "~1.2.3", ">=1.2.3")
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum VersionConstraint {
58    /// Exact version match
59    Exact(Version),
60
61    /// Caret constraint: ^1.2.3 allows >=1.2.3 and <2.0.0
62    Caret(Version),
63
64    /// Tilde constraint: ~1.2.3 allows >=1.2.3 and <1.3.0
65    Tilde(Version),
66
67    /// Greater than or equal
68    GreaterOrEqual(Version),
69
70    /// Less than
71    Less(Version),
72
73    /// Range constraint: >=1.0.0 <2.0.0
74    Range(Version, Version),
75}
76
77impl VersionConstraint {
78    /// Parses a version constraint string
79    pub fn parse(constraint_str: &str) -> Result<Self> {
80        let constraint_str = constraint_str.trim();
81
82        // Check for range constraint first (before checking for >= or <)
83        if constraint_str.contains(" <") {
84            // Range constraint like ">=1.0.0 <2.0.0"
85            let parts: Vec<&str> = constraint_str.split(" <").collect();
86            if parts.len() != 2 {
87                return Err(OrchestrationError::VersionConstraintViolation(format!(
88                    "Invalid range constraint: {}",
89                    constraint_str
90                )));
91            }
92
93            let lower_part = parts[0].trim();
94            let lower = if lower_part.starts_with(">=") {
95                Version::parse(lower_part.strip_prefix(">=").unwrap())?
96            } else if lower_part.starts_with(">") {
97                Version::parse(lower_part.strip_prefix(">").unwrap())?
98            } else {
99                Version::parse(lower_part)?
100            };
101            let upper = Version::parse(parts[1].trim())?;
102            return Ok(VersionConstraint::Range(lower, upper));
103        }
104
105        if let Some(version_str) = constraint_str.strip_prefix('^') {
106            let version = Version::parse(version_str)?;
107            Ok(VersionConstraint::Caret(version))
108        } else if let Some(version_str) = constraint_str.strip_prefix('~') {
109            let version = Version::parse(version_str)?;
110            Ok(VersionConstraint::Tilde(version))
111        } else if let Some(version_str) = constraint_str.strip_prefix(">=") {
112            let version = Version::parse(version_str)?;
113            Ok(VersionConstraint::GreaterOrEqual(version))
114        } else if let Some(version_str) = constraint_str.strip_prefix('<') {
115            let version = Version::parse(version_str)?;
116            Ok(VersionConstraint::Less(version))
117        } else {
118            // Try to parse as exact version
119            let version = Version::parse(constraint_str)?;
120            Ok(VersionConstraint::Exact(version))
121        }
122    }
123
124    /// Checks if a version satisfies this constraint
125    pub fn is_satisfied_by(&self, version: &Version) -> bool {
126        match self {
127            VersionConstraint::Exact(v) => version == v,
128            VersionConstraint::Caret(v) => {
129                // ^1.2.3 allows >=1.2.3 and <2.0.0
130                version >= v && version.major == v.major
131            }
132            VersionConstraint::Tilde(v) => {
133                // ~1.2.3 allows >=1.2.3 and <1.3.0
134                version >= v && version.major == v.major && version.minor == v.minor
135            }
136            VersionConstraint::GreaterOrEqual(v) => version >= v,
137            VersionConstraint::Less(v) => version < v,
138            VersionConstraint::Range(lower, upper) => version >= lower && version < upper,
139        }
140    }
141
142    /// Converts constraint to string
143    pub fn constraint_string(&self) -> String {
144        match self {
145            VersionConstraint::Exact(v) => v.version_string(),
146            VersionConstraint::Caret(v) => format!("^{}", v.version_string()),
147            VersionConstraint::Tilde(v) => format!("~{}", v.version_string()),
148            VersionConstraint::GreaterOrEqual(v) => format!(">={}", v.version_string()),
149            VersionConstraint::Less(v) => format!("<{}", v.version_string()),
150            VersionConstraint::Range(lower, upper) => {
151                format!(">={} <{}", lower.version_string(), upper.version_string())
152            }
153        }
154    }
155}
156
157/// Validates version compatibility between projects
158#[derive(Debug, Clone)]
159pub struct VersionValidator;
160
161impl VersionValidator {
162    /// Checks if a new version is compatible with a constraint
163    pub fn is_compatible(constraint: &str, new_version: &str) -> Result<bool> {
164        let constraint = VersionConstraint::parse(constraint)?;
165        let version = Version::parse(new_version)?;
166
167        Ok(constraint.is_satisfied_by(&version))
168    }
169
170    /// Validates that a new version doesn't break dependent projects
171    pub fn validate_update(
172        _current_version: &str,
173        new_version: &str,
174        dependent_constraints: &[&str],
175    ) -> Result<bool> {
176        let _new_ver = Version::parse(new_version)?;
177
178        // Check if new version satisfies all dependent constraints
179        for constraint_str in dependent_constraints {
180            if !Self::is_compatible(constraint_str, new_version)? {
181                return Err(OrchestrationError::VersionConstraintViolation(format!(
182                    "New version {} does not satisfy constraint {}",
183                    new_version, constraint_str
184                )));
185            }
186        }
187
188        Ok(true)
189    }
190
191    /// Checks if a version update is a breaking change
192    pub fn is_breaking_change(old_version: &str, new_version: &str) -> Result<bool> {
193        let old = Version::parse(old_version)?;
194        let new = Version::parse(new_version)?;
195
196        // Breaking change if major version changes
197        Ok(old.major != new.major)
198    }
199
200    /// Finds compatible versions within a range
201    pub fn find_compatible_versions(
202        constraint: &str,
203        available_versions: &[&str],
204    ) -> Result<Vec<String>> {
205        let constraint = VersionConstraint::parse(constraint)?;
206        let mut compatible = Vec::new();
207
208        for version_str in available_versions {
209            if let Ok(version) = Version::parse(version_str) {
210                if constraint.is_satisfied_by(&version) {
211                    compatible.push(version_str.to_string());
212                }
213            }
214        }
215
216        Ok(compatible)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_version_parse() {
226        let v = Version::parse("1.2.3").unwrap();
227        assert_eq!(v.major, 1);
228        assert_eq!(v.minor, 2);
229        assert_eq!(v.patch, 3);
230    }
231
232    #[test]
233    fn test_version_parse_with_v_prefix() {
234        let v = Version::parse("v1.2.3").unwrap();
235        assert_eq!(v.major, 1);
236        assert_eq!(v.minor, 2);
237        assert_eq!(v.patch, 3);
238    }
239
240    #[test]
241    fn test_version_parse_invalid() {
242        assert!(Version::parse("1.2").is_err());
243        assert!(Version::parse("invalid").is_err());
244    }
245
246    #[test]
247    fn test_version_comparison() {
248        let v1 = Version::parse("1.2.3").unwrap();
249        let v2 = Version::parse("1.2.4").unwrap();
250        let v3 = Version::parse("2.0.0").unwrap();
251
252        assert!(v1 < v2);
253        assert!(v2 < v3);
254        assert!(v1 < v3);
255    }
256
257    #[test]
258    fn test_caret_constraint() {
259        let constraint = VersionConstraint::parse("^1.2.3").unwrap();
260
261        assert!(constraint.is_satisfied_by(&Version::parse("1.2.3").unwrap()));
262        assert!(constraint.is_satisfied_by(&Version::parse("1.2.4").unwrap()));
263        assert!(constraint.is_satisfied_by(&Version::parse("1.3.0").unwrap()));
264        assert!(!constraint.is_satisfied_by(&Version::parse("2.0.0").unwrap()));
265        assert!(!constraint.is_satisfied_by(&Version::parse("1.2.2").unwrap()));
266    }
267
268    #[test]
269    fn test_tilde_constraint() {
270        let constraint = VersionConstraint::parse("~1.2.3").unwrap();
271
272        assert!(constraint.is_satisfied_by(&Version::parse("1.2.3").unwrap()));
273        assert!(constraint.is_satisfied_by(&Version::parse("1.2.4").unwrap()));
274        assert!(!constraint.is_satisfied_by(&Version::parse("1.3.0").unwrap()));
275        assert!(!constraint.is_satisfied_by(&Version::parse("2.0.0").unwrap()));
276    }
277
278    #[test]
279    fn test_greater_or_equal_constraint() {
280        let constraint = VersionConstraint::parse(">=1.2.3").unwrap();
281
282        assert!(constraint.is_satisfied_by(&Version::parse("1.2.3").unwrap()));
283        assert!(constraint.is_satisfied_by(&Version::parse("1.2.4").unwrap()));
284        assert!(constraint.is_satisfied_by(&Version::parse("2.0.0").unwrap()));
285        assert!(!constraint.is_satisfied_by(&Version::parse("1.2.2").unwrap()));
286    }
287
288    #[test]
289    fn test_less_constraint() {
290        let constraint = VersionConstraint::parse("<2.0.0").unwrap();
291
292        assert!(constraint.is_satisfied_by(&Version::parse("1.2.3").unwrap()));
293        assert!(constraint.is_satisfied_by(&Version::parse("1.9.9").unwrap()));
294        assert!(!constraint.is_satisfied_by(&Version::parse("2.0.0").unwrap()));
295        assert!(!constraint.is_satisfied_by(&Version::parse("2.0.1").unwrap()));
296    }
297
298    #[test]
299    fn test_range_constraint() {
300        // Range constraints are parsed as ">=X.Y.Z <A.B.C"
301        let constraint = VersionConstraint::parse(">=1.0.0 <2.0.0").unwrap();
302
303        assert!(constraint.is_satisfied_by(&Version::parse("1.0.0").unwrap()));
304        assert!(constraint.is_satisfied_by(&Version::parse("1.5.0").unwrap()));
305        assert!(constraint.is_satisfied_by(&Version::parse("1.9.9").unwrap()));
306        assert!(!constraint.is_satisfied_by(&Version::parse("0.9.9").unwrap()));
307        assert!(!constraint.is_satisfied_by(&Version::parse("2.0.0").unwrap()));
308    }
309
310    #[test]
311    fn test_exact_constraint() {
312        let constraint = VersionConstraint::parse("1.2.3").unwrap();
313
314        assert!(constraint.is_satisfied_by(&Version::parse("1.2.3").unwrap()));
315        assert!(!constraint.is_satisfied_by(&Version::parse("1.2.4").unwrap()));
316        assert!(!constraint.is_satisfied_by(&Version::parse("1.2.2").unwrap()));
317    }
318
319    #[test]
320    fn test_is_compatible() {
321        assert!(VersionValidator::is_compatible("^1.2.3", "1.2.4").unwrap());
322        assert!(VersionValidator::is_compatible("^1.2.3", "1.3.0").unwrap());
323        assert!(!VersionValidator::is_compatible("^1.2.3", "2.0.0").unwrap());
324    }
325
326    #[test]
327    fn test_validate_update() {
328        let constraints = vec!["^1.0.0", "~1.2.0"];
329        assert!(VersionValidator::validate_update("1.2.3", "1.2.4", &constraints).unwrap());
330        assert!(VersionValidator::validate_update("1.2.3", "1.3.0", &constraints).is_err());
331    }
332
333    #[test]
334    fn test_is_breaking_change() {
335        assert!(!VersionValidator::is_breaking_change("1.2.3", "1.2.4").unwrap());
336        assert!(!VersionValidator::is_breaking_change("1.2.3", "1.3.0").unwrap());
337        assert!(VersionValidator::is_breaking_change("1.2.3", "2.0.0").unwrap());
338    }
339
340    #[test]
341    fn test_find_compatible_versions() {
342        let available = vec!["1.0.0", "1.2.3", "1.2.4", "1.3.0", "2.0.0"];
343        let compatible = VersionValidator::find_compatible_versions("^1.2.3", &available).unwrap();
344
345        // ^1.2.3 allows >=1.2.3 and <2.0.0, so 1.2.3, 1.2.4, and 1.3.0 are compatible
346        assert_eq!(compatible.len(), 3);
347        assert!(compatible.contains(&"1.2.3".to_string()));
348        assert!(compatible.contains(&"1.2.4".to_string()));
349        assert!(compatible.contains(&"1.3.0".to_string()));
350    }
351}