Skip to main content

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