cargo_util_schemas/manifest/
rust_version.rs

1use std::fmt;
2use std::fmt::Display;
3
4use serde_untagged::UntaggedEnumVisitor;
5
6use crate::core::PartialVersion;
7use crate::core::PartialVersionError;
8
9#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize)]
10#[serde(transparent)]
11pub struct RustVersion(PartialVersion);
12
13impl RustVersion {
14    pub fn is_compatible_with(&self, rustc: &PartialVersion) -> bool {
15        let msrv = self.0.to_caret_req();
16        // Remove any pre-release identifiers for easier comparison
17        let rustc = semver::Version {
18            major: rustc.major,
19            minor: rustc.minor.unwrap_or_default(),
20            patch: rustc.patch.unwrap_or_default(),
21            pre: Default::default(),
22            build: Default::default(),
23        };
24        msrv.matches(&rustc)
25    }
26
27    pub fn into_partial(self) -> PartialVersion {
28        self.0
29    }
30
31    pub fn as_partial(&self) -> &PartialVersion {
32        &self.0
33    }
34}
35
36impl std::str::FromStr for RustVersion {
37    type Err = RustVersionError;
38
39    fn from_str(value: &str) -> Result<Self, Self::Err> {
40        let partial = value.parse::<PartialVersion>();
41        let partial = partial.map_err(RustVersionErrorKind::PartialVersion)?;
42        partial.try_into()
43    }
44}
45
46impl TryFrom<semver::Version> for RustVersion {
47    type Error = RustVersionError;
48
49    fn try_from(version: semver::Version) -> Result<Self, Self::Error> {
50        let version = PartialVersion::from(version);
51        Self::try_from(version)
52    }
53}
54
55impl TryFrom<PartialVersion> for RustVersion {
56    type Error = RustVersionError;
57
58    fn try_from(partial: PartialVersion) -> Result<Self, Self::Error> {
59        if partial.pre.is_some() {
60            return Err(RustVersionErrorKind::Prerelease.into());
61        }
62        if partial.build.is_some() {
63            return Err(RustVersionErrorKind::BuildMetadata.into());
64        }
65        Ok(Self(partial))
66    }
67}
68
69impl<'de> serde::Deserialize<'de> for RustVersion {
70    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
71    where
72        D: serde::Deserializer<'de>,
73    {
74        UntaggedEnumVisitor::new()
75            .expecting("SemVer version")
76            .string(|value| value.parse().map_err(serde::de::Error::custom))
77            .deserialize(deserializer)
78    }
79}
80
81impl Display for RustVersion {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        self.0.fmt(f)
84    }
85}
86
87/// Error parsing a [`RustVersion`].
88#[derive(Debug, thiserror::Error)]
89#[error(transparent)]
90pub struct RustVersionError(#[from] RustVersionErrorKind);
91
92/// Non-public error kind for [`RustVersionError`].
93#[non_exhaustive]
94#[derive(Debug, thiserror::Error)]
95enum RustVersionErrorKind {
96    #[error("unexpected prerelease field, expected a version like \"1.32\"")]
97    Prerelease,
98
99    #[error("unexpected build field, expected a version like \"1.32\"")]
100    BuildMetadata,
101
102    #[error(transparent)]
103    PartialVersion(#[from] PartialVersionError),
104}
105
106#[cfg(test)]
107mod test {
108    use super::*;
109    use snapbox::prelude::*;
110    use snapbox::str;
111
112    #[test]
113    fn is_compatible_with_rustc() {
114        let cases = &[
115            ("1", "1.70.0", true),
116            ("1.30", "1.70.0", true),
117            ("1.30.10", "1.70.0", true),
118            ("1.70", "1.70.0", true),
119            ("1.70.0", "1.70.0", true),
120            ("1.70.1", "1.70.0", false),
121            ("1.70", "1.70.0-nightly", true),
122            ("1.70.0", "1.70.0-nightly", true),
123            ("1.71", "1.70.0", false),
124            ("2", "1.70.0", false),
125        ];
126        let mut passed = true;
127        for (msrv, rustc, expected) in cases {
128            let msrv: RustVersion = msrv.parse().unwrap();
129            let rustc = PartialVersion::from(semver::Version::parse(rustc).unwrap());
130            if msrv.is_compatible_with(&rustc) != *expected {
131                println!("failed: {msrv} is_compatible_with {rustc} == {expected}");
132                passed = false;
133            }
134        }
135        assert!(passed);
136    }
137
138    #[test]
139    fn is_compatible_with_workspace_msrv() {
140        let cases = &[
141            ("1", "1", true),
142            ("1", "1.70", true),
143            ("1", "1.70.0", true),
144            ("1.30", "1", false),
145            ("1.30", "1.70", true),
146            ("1.30", "1.70.0", true),
147            ("1.30.10", "1", false),
148            ("1.30.10", "1.70", true),
149            ("1.30.10", "1.70.0", true),
150            ("1.70", "1", false),
151            ("1.70", "1.70", true),
152            ("1.70", "1.70.0", true),
153            ("1.70.0", "1", false),
154            ("1.70.0", "1.70", true),
155            ("1.70.0", "1.70.0", true),
156            ("1.70.1", "1", false),
157            ("1.70.1", "1.70", false),
158            ("1.70.1", "1.70.0", false),
159            ("1.71", "1", false),
160            ("1.71", "1.70", false),
161            ("1.71", "1.70.0", false),
162            ("2", "1.70.0", false),
163        ];
164        let mut passed = true;
165        for (dep_msrv, ws_msrv, expected) in cases {
166            let dep_msrv: RustVersion = dep_msrv.parse().unwrap();
167            let ws_msrv = ws_msrv.parse::<RustVersion>().unwrap().into_partial();
168            if dep_msrv.is_compatible_with(&ws_msrv) != *expected {
169                println!("failed: {dep_msrv} is_compatible_with {ws_msrv} == {expected}");
170                passed = false;
171            }
172        }
173        assert!(passed);
174    }
175
176    #[test]
177    fn parse_errors() {
178        let cases = &[
179            // Disallow caret
180            (
181                "^1.43",
182                str![[r#"unexpected version requirement, expected a version like "1.32""#]],
183            ),
184            // Valid pre-release
185            (
186                "1.43.0-beta.1",
187                str![[r#"unexpected prerelease field, expected a version like "1.32""#]],
188            ),
189            // Bad pre-release
190            (
191                "1.43-beta.1",
192                str![[r#"unexpected prerelease field, expected a version like "1.32""#]],
193            ),
194            // Weird wildcard
195            (
196                "x",
197                str![[r#"unexpected version requirement, expected a version like "1.32""#]],
198            ),
199            (
200                "1.x",
201                str![[r#"unexpected version requirement, expected a version like "1.32""#]],
202            ),
203            (
204                "1.1.x",
205                str![[r#"unexpected version requirement, expected a version like "1.32""#]],
206            ),
207            // Non-sense
208            ("foodaddle", str![[r#"expected a version like "1.32""#]]),
209        ];
210        for (input, expected) in cases {
211            let actual: Result<RustVersion, _> = input.parse();
212            let actual = match actual {
213                Ok(result) => format!("didn't fail: {result:?}"),
214                Err(err) => err.to_string(),
215            };
216            snapbox::assert_data_eq!(actual, expected.clone().raw());
217        }
218    }
219}