Skip to main content

mars_agents/resolve/
compat.rs

1use super::VersionConstraint;
2use semver::Version;
3
4/// Result of comparing two version constraints.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum CompatibilityResult {
7    /// Both constraints resolve to the same version.
8    Compatible,
9    /// Constraints might resolve differently (Latest vs pinned).
10    PotentiallyConflicting,
11    /// Constraints cannot resolve to the same version.
12    Conflicting,
13}
14
15impl VersionConstraint {
16    /// Check if this constraint is compatible with another.
17    ///
18    /// Matrix:
19    /// - Latest + Latest => Compatible
20    /// - Semver(same) + Semver(same) => Compatible
21    /// - RefPin(same) + RefPin(same) => Compatible
22    /// - Latest + Semver/RefPin => PotentiallyConflicting
23    /// - Different Semver/RefPin => Conflicting
24    /// - Semver + RefPin => Conflicting
25    pub fn compatible_with(&self, other: &VersionConstraint) -> CompatibilityResult {
26        use CompatibilityResult::{Compatible, Conflicting, PotentiallyConflicting};
27        use VersionConstraint::{Latest, RefPin, Semver};
28
29        match (self, other) {
30            (Latest, Latest) => Compatible,
31            (Latest, Semver(_) | RefPin(_)) | (Semver(_) | RefPin(_), Latest) => {
32                PotentiallyConflicting
33            }
34            (Semver(lhs), Semver(rhs)) => {
35                if lhs == rhs {
36                    Compatible
37                } else {
38                    Conflicting
39                }
40            }
41            (RefPin(lhs), RefPin(rhs)) => {
42                if lhs == rhs {
43                    Compatible
44                } else {
45                    Conflicting
46                }
47            }
48            (Semver(_), RefPin(_)) | (RefPin(_), Semver(_)) => Conflicting,
49        }
50    }
51
52    /// Check compatibility against a concrete resolved version.
53    ///
54    /// This is stricter than pure syntactic comparison for semver constraints:
55    /// two different semver expressions are compatible when both accept the
56    /// already-resolved concrete version.
57    pub fn compatible_with_resolved(
58        &self,
59        other: &VersionConstraint,
60        resolved_version: Option<&Version>,
61    ) -> CompatibilityResult {
62        use CompatibilityResult::{Compatible, Conflicting};
63        use VersionConstraint::Semver;
64
65        match (self, other) {
66            (Semver(lhs), Semver(rhs)) => {
67                if lhs == rhs {
68                    Compatible
69                } else if let Some(version) = resolved_version {
70                    if lhs.matches(version) && rhs.matches(version) {
71                        Compatible
72                    } else {
73                        Conflicting
74                    }
75                } else {
76                    Conflicting
77                }
78            }
79            _ => self.compatible_with(other),
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::CompatibilityResult;
87    use crate::resolve::VersionConstraint;
88    use semver::Version;
89
90    fn semver(req: &str) -> VersionConstraint {
91        VersionConstraint::Semver(req.parse().expect("valid semver requirement"))
92    }
93
94    #[test]
95    fn latest_with_latest_is_compatible() {
96        assert_eq!(
97            VersionConstraint::Latest.compatible_with(&VersionConstraint::Latest),
98            CompatibilityResult::Compatible
99        );
100    }
101
102    #[test]
103    fn same_semver_is_compatible() {
104        assert_eq!(
105            semver("^1.2").compatible_with(&semver("^1.2")),
106            CompatibilityResult::Compatible
107        );
108    }
109
110    #[test]
111    fn same_ref_pin_is_compatible() {
112        assert_eq!(
113            VersionConstraint::RefPin("main".into())
114                .compatible_with(&VersionConstraint::RefPin("main".into())),
115            CompatibilityResult::Compatible
116        );
117    }
118
119    #[test]
120    fn latest_with_semver_is_potentially_conflicting() {
121        assert_eq!(
122            VersionConstraint::Latest.compatible_with(&semver(">=1.0.0")),
123            CompatibilityResult::PotentiallyConflicting
124        );
125    }
126
127    #[test]
128    fn latest_with_ref_pin_is_potentially_conflicting() {
129        assert_eq!(
130            VersionConstraint::Latest.compatible_with(&VersionConstraint::RefPin("main".into())),
131            CompatibilityResult::PotentiallyConflicting
132        );
133    }
134
135    #[test]
136    fn different_semver_is_conflicting() {
137        assert_eq!(
138            semver("^1.0").compatible_with(&semver("^2.0")),
139            CompatibilityResult::Conflicting
140        );
141    }
142
143    #[test]
144    fn different_ref_pin_is_conflicting() {
145        assert_eq!(
146            VersionConstraint::RefPin("main".into())
147                .compatible_with(&VersionConstraint::RefPin("release".into())),
148            CompatibilityResult::Conflicting
149        );
150    }
151
152    #[test]
153    fn semver_with_ref_pin_is_conflicting() {
154        assert_eq!(
155            semver("^1.0").compatible_with(&VersionConstraint::RefPin("main".into())),
156            CompatibilityResult::Conflicting
157        );
158    }
159
160    #[test]
161    fn equivalent_semver_syntax_is_compatible_for_resolved_version() {
162        let resolved = Version::new(1, 4, 2);
163        assert_eq!(
164            semver("^1.0").compatible_with_resolved(&semver(">=1.0.0, <2.0.0"), Some(&resolved)),
165            CompatibilityResult::Compatible
166        );
167    }
168
169    #[test]
170    fn incompatible_semver_syntax_is_conflicting_for_resolved_version() {
171        let resolved = Version::new(2, 0, 0);
172        assert_eq!(
173            semver("^1.0").compatible_with_resolved(&semver(">=1.0.0, <2.0.0"), Some(&resolved)),
174            CompatibilityResult::Conflicting
175        );
176    }
177}