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