cargo_util_schemas/manifest/
rust_version.rs1use 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 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#[derive(Debug, thiserror::Error)]
127#[error(transparent)]
128pub struct RustVersionError(#[from] RustVersionErrorKind);
129
130#[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 (
219 "^1.43",
220 str![[r#"unexpected version requirement, expected a version like "1.32""#]],
221 ),
222 (
224 "1.43.0-beta.1",
225 str![[r#"unexpected prerelease field, expected a version like "1.32""#]],
226 ),
227 (
229 "1.43-beta.1",
230 str![[r#"unexpected prerelease field, expected a version like "1.32""#]],
231 ),
232 (
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 ("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}