1use crate::parsers::modular;
2use crate::{BaseVersion, FullVersionParser, ParserError};
3#[cfg(feature = "semver")]
4use std::convert::TryFrom;
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub struct FullVersion {
32 pub major: u64,
38 pub minor: u64,
44 pub patch: u64,
46}
47
48impl FullVersion {
49 pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
55 Self {
56 major,
57 minor,
58 patch,
59 }
60 }
61
62 pub fn parse(input: &str) -> Result<Self, ParserError> {
66 modular::ModularParser.parse_full(input)
67 }
68
69 pub fn to_base_version_lossy(self) -> BaseVersion {
73 BaseVersion {
74 major: self.major,
75 minor: self.minor,
76 }
77 }
78
79 pub fn map<U, F>(self, fun: F) -> U
96 where
97 F: FnOnce(Self) -> U,
98 {
99 fun(self)
100 }
101}
102
103#[cfg(feature = "semver")]
104impl From<FullVersion> for semver::Version {
105 fn from(version: FullVersion) -> Self {
123 semver::Version::new(version.major, version.minor, version.patch)
124 }
125}
126
127#[cfg(feature = "semver")]
128impl TryFrom<&semver::Version> for FullVersion {
129 type Error = FromSemverError;
130
131 fn try_from(version: &semver::Version) -> Result<Self, Self::Error> {
156 if !version.pre.is_empty() {
157 return Err(FromSemverError::PreRelease(version.pre.clone()));
158 }
159
160 if !version.build.is_empty() {
161 return Err(FromSemverError::BuildMetadata(version.build.clone()));
162 }
163
164 Ok(FullVersion::new(
165 version.major,
166 version.minor,
167 version.patch,
168 ))
169 }
170}
171
172#[cfg(feature = "semver")]
177#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
178pub enum FromSemverError {
179 #[error("Pre-release labels are not supported, but got '{0}'")]
181 PreRelease(semver::Prerelease),
182
183 #[error("Build metadata labels are not supported, but got '{0}'")]
185 BuildMetadata(semver::BuildMetadata),
186}
187
188impl From<(u64, u64, u64)> for FullVersion {
189 fn from(tuple: (u64, u64, u64)) -> Self {
190 FullVersion {
191 major: tuple.0,
192 minor: tuple.1,
193 patch: tuple.2,
194 }
195 }
196}
197
198impl fmt::Display for FullVersion {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 f.write_fmt(format_args!("{}.{}.{}", self.major, self.minor, self.patch))
201 }
202}
203
204impl FromStr for FullVersion {
205 type Err = ParserError;
206
207 fn from_str(input: &str) -> Result<Self, Self::Err> {
208 Self::parse(input)
209 }
210}
211
212#[cfg(all(test, feature = "semver"))]
213mod semver_tests {
214 use crate::{FromSemverError, FullVersion};
215 use std::convert::TryFrom;
216
217 #[yare::parameterized(
218 zeros = { "0.0.0", FullVersion::new(0, 0, 0) },
219 regular = { "1.2.3", FullVersion::new(1, 2, 3) },
220 large = { "18446744073709551615.0.1", FullVersion::new(u64::MAX, 0, 1) },
221 )]
222 fn try_from_semver_ok(input: &str, expected: FullVersion) {
223 let version = semver::Version::parse(input).unwrap();
224
225 assert_eq!(FullVersion::try_from(&version).unwrap(), expected);
226 }
227
228 #[yare::parameterized(
229 nightly = { "1.2.3-nightly", "nightly" },
230 beta = { "1.2.3-beta.1", "beta.1" },
231 pre_release_and_build = { "1.2.3-beta.1+abc", "beta.1" },
232 )]
233 fn try_from_semver_pre_release(input: &str, expected: &str) {
234 let version = semver::Version::parse(input).unwrap();
235
236 assert_eq!(
237 FullVersion::try_from(&version).unwrap_err(),
238 FromSemverError::PreRelease(semver::Prerelease::new(expected).unwrap())
239 );
240 }
241
242 #[test]
243 fn try_from_semver_build_metadata() {
244 let version = semver::Version::parse("1.2.3+abc").unwrap();
245
246 assert_eq!(
247 FullVersion::try_from(&version).unwrap_err(),
248 FromSemverError::BuildMetadata(semver::BuildMetadata::new("abc").unwrap())
249 );
250 }
251
252 #[test]
253 fn round_trip() {
254 let version = FullVersion::new(1, 2, 3);
255 let converted = semver::Version::from(version);
256
257 assert_eq!(FullVersion::try_from(&converted).unwrap(), version);
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use crate::{BaseVersion, FullVersion};
264
265 #[test]
266 fn from_tuple() {
267 let major = 0;
268 let minor = 1;
269 let patch = 2;
270
271 assert_eq!(
272 FullVersion {
273 major,
274 minor,
275 patch
276 },
277 FullVersion::from((major, minor, patch))
278 );
279 }
280
281 #[yare::parameterized(
282 zeros = { FullVersion { major: 0, minor: 0, patch: 0 }, "0.0.0" },
283 non_zero = { FullVersion { major: 1, minor: 2, patch: 3 }, "1.2.3" },
284 )]
285 fn display(base_version: FullVersion, expected: &str) {
286 let displayed = format!("{}", base_version);
287
288 assert_eq!(&displayed, expected);
289 }
290
291 #[test]
292 fn to_base_version_lossy() {
293 let full = FullVersion {
294 major: 1,
295 minor: 2,
296 patch: 3,
297 };
298 let converted = full.to_base_version_lossy();
299
300 assert_eq!(BaseVersion { major: 1, minor: 2 }, converted)
301 }
302
303 #[test]
304 fn map() {
305 let version = BaseVersion::new(1, 2);
306 let mapped = version.map(|v| ("Everything is awesome", v.major, v.minor));
307
308 assert_eq!(mapped.1, 1);
309 assert_eq!(mapped.2, 2);
310 }
311}
312
313#[cfg(test)]
314mod ord_tests {
315 use crate::FullVersion;
316 use std::cmp::Ordering;
317
318 #[yare::parameterized(
319 zero = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
320 ones = { FullVersion { major: 1, minor: 1, patch: 1 }, FullVersion { major: 1, minor: 1, patch: 1 } },
321 )]
322 fn equals(lhs: FullVersion, rhs: FullVersion) {
323 assert_eq!(lhs.cmp(&rhs), Ordering::Equal);
324 }
325
326 #[yare::parameterized(
327 major_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 1, minor: 0, patch: 0 } },
328 minor_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 1, patch: 0 } },
329 patch_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 1 } },
330 )]
331 fn less(lhs: FullVersion, rhs: FullVersion) {
332 assert_eq!(lhs.cmp(&rhs), Ordering::Less);
333 }
334
335 #[yare::parameterized(
336 major_by_1 = { FullVersion { major: 1, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
337 minor_by_1 = { FullVersion { major: 0, minor: 1, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
338 patch_by_1 = { FullVersion { major: 0, minor: 0, patch: 1 }, FullVersion { major: 0, minor: 0, patch: 0 } },
339 )]
340 fn greater(lhs: FullVersion, rhs: FullVersion) {
341 assert_eq!(lhs.cmp(&rhs), Ordering::Greater);
342 }
343}
344
345#[cfg(test)]
346mod partial_ord_tests {
347 use crate::FullVersion;
348 use std::cmp::Ordering;
349
350 #[yare::parameterized(
351 zero = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
352 ones = { FullVersion { major: 1, minor: 1, patch: 1 }, FullVersion { major: 1, minor: 1, patch: 1 } },
353 )]
354 fn equals(lhs: FullVersion, rhs: FullVersion) {
355 assert_eq!(lhs.partial_cmp(&rhs), Some(Ordering::Equal));
356 }
357
358 #[yare::parameterized(
359 major_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 1, minor: 0, patch: 0 } },
360 minor_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 1, patch: 0 } },
361 patch_by_1 = { FullVersion { major: 0, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 1 } },
362 )]
363 fn less(lhs: FullVersion, rhs: FullVersion) {
364 assert_eq!(lhs.partial_cmp(&rhs), Some(Ordering::Less));
365 }
366
367 #[yare::parameterized(
368 major_by_1 = { FullVersion { major: 1, minor: 0, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
369 minor_by_1 = { FullVersion { major: 0, minor: 1, patch: 0 }, FullVersion { major: 0, minor: 0, patch: 0 } },
370 patch_by_1 = { FullVersion { major: 0, minor: 0, patch: 1 }, FullVersion { major: 0, minor: 0, patch: 0 } },
371 )]
372 fn greater(lhs: FullVersion, rhs: FullVersion) {
373 assert_eq!(lhs.partial_cmp(&rhs), Some(Ordering::Greater));
374 }
375}
376
377#[cfg(test)]
378mod parse_full {
379 use crate::parsers::NumericError;
380 use crate::parsers::error::ExpectedError;
381 use crate::{FullVersion, ParserError};
382
383 #[test]
384 fn ok() {
385 let version = FullVersion::parse("1.2.3").unwrap();
386
387 assert_eq!(version, FullVersion::new(1, 2, 3));
388 }
389
390 #[test]
391 fn err_on_base_only() {
392 let result = FullVersion::parse("1.2");
393
394 assert!(matches!(
395 result.unwrap_err(),
396 ParserError::Expected(ExpectedError::Separator { .. })
397 ));
398 }
399
400 #[test]
401 fn err_on_not_finished() {
402 let result = FullVersion::parse("1.2.3.");
403
404 assert!(matches!(
405 result.unwrap_err(),
406 ParserError::Expected(ExpectedError::EndOfInput { .. })
407 ));
408 }
409
410 #[test]
411 fn err_on_starts_with_0() {
412 let result = FullVersion::parse("1.2.03");
413
414 assert!(matches!(
415 result.unwrap_err(),
416 ParserError::Numeric(NumericError::LeadingZero)
417 ));
418 }
419}