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)]
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 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#[derive(Debug, thiserror::Error)]
129#[error(transparent)]
130pub struct RustVersionError(#[from] RustVersionErrorKind);
131
132#[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 (
221 "^1.43",
222 str![[r#"unexpected version requirement, expected a version like "1.32""#]],
223 ),
224 (
226 "1.43.0-beta.1",
227 str![[r#"unexpected prerelease field, expected a version like "1.32""#]],
228 ),
229 (
231 "1.43-beta.1",
232 str![[r#"unexpected prerelease field, expected a version like "1.32""#]],
233 ),
234 (
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 ("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}