1use std::fmt::{self, Display};
2
3use semver::{Comparator, Version, VersionReq};
4use serde_untagged::UntaggedEnumVisitor;
5
6#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug)]
7pub struct PartialVersion {
8 pub major: u64,
9 pub minor: Option<u64>,
10 pub patch: Option<u64>,
11 pub pre: Option<semver::Prerelease>,
12 pub build: Option<semver::BuildMetadata>,
13}
14
15impl PartialVersion {
16 pub fn to_version(&self) -> Option<Version> {
17 Some(Version {
18 major: self.major,
19 minor: self.minor?,
20 patch: self.patch?,
21 pre: self.pre.clone().unwrap_or_default(),
22 build: self.build.clone().unwrap_or_default(),
23 })
24 }
25
26 pub fn to_caret_req(&self) -> VersionReq {
27 VersionReq {
28 comparators: vec![Comparator {
29 op: semver::Op::Caret,
30 major: self.major,
31 minor: self.minor,
32 patch: self.patch,
33 pre: self.pre.as_ref().cloned().unwrap_or_default(),
34 }],
35 }
36 }
37
38 pub fn matches(&self, version: &Version) -> bool {
43 if !version.pre.is_empty() && self.pre.is_none() {
44 return false;
47 }
48 self.major == version.major
49 && self.minor.map(|f| f == version.minor).unwrap_or(true)
50 && self.patch.map(|f| f == version.patch).unwrap_or(true)
51 && self.pre.as_ref().map(|f| f == &version.pre).unwrap_or(true)
52 && self
53 .build
54 .as_ref()
55 .map(|f| f == &version.build)
56 .unwrap_or(true)
57 }
58}
59
60impl From<semver::Version> for PartialVersion {
61 fn from(ver: semver::Version) -> Self {
62 let pre = if ver.pre.is_empty() {
63 None
64 } else {
65 Some(ver.pre)
66 };
67 let build = if ver.build.is_empty() {
68 None
69 } else {
70 Some(ver.build)
71 };
72 Self {
73 major: ver.major,
74 minor: Some(ver.minor),
75 patch: Some(ver.patch),
76 pre,
77 build,
78 }
79 }
80}
81
82impl std::str::FromStr for PartialVersion {
83 type Err = PartialVersionError;
84
85 fn from_str(value: &str) -> Result<Self, Self::Err> {
86 match semver::Version::parse(value) {
87 Ok(ver) => Ok(ver.into()),
88 Err(_) => {
89 let mut version_req = match semver::VersionReq::parse(value) {
91 Ok(req) => req,
92 Err(_) if value.contains('-') => return Err(ErrorKind::Prerelease.into()),
93 Err(_) if value.contains('+') => return Err(ErrorKind::BuildMetadata.into()),
94 Err(_) => return Err(ErrorKind::Unexpected.into()),
95 };
96 if version_req.comparators.len() != 1 {
97 return Err(ErrorKind::VersionReq.into());
98 }
99 let comp = version_req.comparators.pop().unwrap();
100 if comp.op != semver::Op::Caret {
101 return Err(ErrorKind::VersionReq.into());
102 } else if value.starts_with('^') {
103 return Err(ErrorKind::VersionReq.into());
105 }
106 let pre = if comp.pre.is_empty() {
107 None
108 } else {
109 Some(comp.pre)
110 };
111 Ok(Self {
112 major: comp.major,
113 minor: comp.minor,
114 patch: comp.patch,
115 pre,
116 build: None,
117 })
118 }
119 }
120 }
121}
122
123impl Display for PartialVersion {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 let major = self.major;
126 write!(f, "{major}")?;
127 if let Some(minor) = self.minor {
128 write!(f, ".{minor}")?;
129 }
130 if let Some(patch) = self.patch {
131 write!(f, ".{patch}")?;
132 }
133 if let Some(pre) = self.pre.as_ref() {
134 write!(f, "-{pre}")?;
135 }
136 if let Some(build) = self.build.as_ref() {
137 write!(f, "+{build}")?;
138 }
139 Ok(())
140 }
141}
142
143impl serde::Serialize for PartialVersion {
144 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
145 where
146 S: serde::Serializer,
147 {
148 serializer.collect_str(self)
149 }
150}
151
152impl<'de> serde::Deserialize<'de> for PartialVersion {
153 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
154 where
155 D: serde::Deserializer<'de>,
156 {
157 UntaggedEnumVisitor::new()
158 .expecting("SemVer version")
159 .string(|value| value.parse().map_err(serde::de::Error::custom))
160 .deserialize(deserializer)
161 }
162}
163
164#[derive(Debug, thiserror::Error)]
166#[error(transparent)]
167pub struct PartialVersionError(#[from] ErrorKind);
168
169#[non_exhaustive]
171#[derive(Debug, thiserror::Error)]
172enum ErrorKind {
173 #[error("unexpected version requirement, expected a version like \"1.32\"")]
174 VersionReq,
175
176 #[error("unexpected prerelease field, expected a version like \"1.32\"")]
177 Prerelease,
178
179 #[error("unexpected build field, expected a version like \"1.32\"")]
180 BuildMetadata,
181
182 #[error("expected a version like \"1.32\"")]
183 Unexpected,
184}
185
186#[cfg(test)]
187mod test {
188 use super::*;
189 use snapbox::prelude::*;
190 use snapbox::str;
191
192 #[test]
193 fn parse_success() {
194 let cases = &[
195 ("1.43.0-beta.1", str!["1.43.0-beta.1"]),
197 ("1.43.0-beta.1.x", str!["1.43.0-beta.1.x"]),
199 ];
200 for (input, expected) in cases {
201 let actual: Result<PartialVersion, _> = input.parse();
202 let actual = match actual {
203 Ok(result) => result.to_string(),
204 Err(err) => format!("didn't pass: {err}"),
205 };
206 snapbox::assert_data_eq!(actual, expected.clone().raw());
207 }
208 }
209
210 #[test]
211 fn parse_errors() {
212 let cases = &[
213 (
215 "^1.43",
216 str![[r#"unexpected version requirement, expected a version like "1.32""#]],
217 ),
218 (
220 "1.43-beta.1",
221 str![[r#"unexpected prerelease field, expected a version like "1.32""#]],
222 ),
223 (
225 "x",
226 str![[r#"unexpected version requirement, expected a version like "1.32""#]],
227 ),
228 (
229 "1.x",
230 str![[r#"unexpected version requirement, expected a version like "1.32""#]],
231 ),
232 (
233 "1.1.x",
234 str![[r#"unexpected version requirement, expected a version like "1.32""#]],
235 ),
236 ("foodaddle", str![[r#"expected a version like "1.32""#]]),
238 ];
239 for (input, expected) in cases {
240 let actual: Result<PartialVersion, _> = input.parse();
241 let actual = match actual {
242 Ok(result) => format!("didn't fail: {result:?}"),
243 Err(err) => err.to_string(),
244 };
245 snapbox::assert_data_eq!(actual, expected.clone().raw());
246 }
247 }
248}