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 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#[derive(Debug, thiserror::Error)]
89#[error(transparent)]
90pub struct RustVersionError(#[from] RustVersionErrorKind);
91
92#[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 (
181 "^1.43",
182 str![[r#"unexpected version requirement, expected a version like "1.32""#]],
183 ),
184 (
186 "1.43.0-beta.1",
187 str![[r#"unexpected prerelease field, expected a version like "1.32""#]],
188 ),
189 (
191 "1.43-beta.1",
192 str![[r#"unexpected prerelease field, expected a version like "1.32""#]],
193 ),
194 (
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 ("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}