Skip to main content

use_version/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Composable semantic-version primitives for RustUse.
5
6use std::{cmp::Ordering, error::Error, fmt};
7
8use semver::{BuildMetadata, Prerelease};
9use serde::{Deserialize, Serialize};
10
11/// A typed semantic version value.
12#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13pub struct Version(pub semver::Version);
14
15impl Version {
16    /// Returns the wrapped semantic version.
17    #[must_use]
18    pub fn as_semver(&self) -> &semver::Version {
19        &self.0
20    }
21}
22
23impl fmt::Display for Version {
24    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25        self.0.fmt(formatter)
26    }
27}
28
29/// The kind of version bump to apply.
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
31pub enum VersionBump {
32    Patch,
33    Minor,
34    Major,
35}
36
37/// A simple version policy marker for RustUse release flows.
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
39pub enum VersionPolicy {
40    StrictSemver,
41    RustUseDefault,
42}
43
44/// The release level represented by a version transition.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
46pub enum ReleaseLevel {
47    Patch,
48    Minor,
49    Major,
50    PreRelease,
51}
52
53/// Errors that can occur while parsing semantic versions.
54#[derive(Debug)]
55pub struct VersionError(semver::Error);
56
57impl fmt::Display for VersionError {
58    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59        write!(formatter, "failed to parse semantic version: {}", self.0)
60    }
61}
62
63impl Error for VersionError {
64    fn source(&self) -> Option<&(dyn Error + 'static)> {
65        Some(&self.0)
66    }
67}
68
69impl From<semver::Error> for VersionError {
70    fn from(error: semver::Error) -> Self {
71        Self(error)
72    }
73}
74
75/// Parses a semantic version string.
76pub fn parse_version(value: &str) -> Result<Version, VersionError> {
77    Ok(Version(semver::Version::parse(value)?))
78}
79
80/// Returns `true` when a version contains prerelease metadata.
81#[must_use]
82pub fn is_prerelease(version: &Version) -> bool {
83    !version.0.pre.is_empty()
84}
85
86/// Returns the next patch version.
87#[must_use]
88pub fn next_patch(version: &Version) -> Version {
89    let mut next = version.0.clone();
90    next.patch += 1;
91    next.pre = Prerelease::EMPTY;
92    next.build = BuildMetadata::EMPTY;
93    Version(next)
94}
95
96/// Returns the next minor version.
97#[must_use]
98pub fn next_minor(version: &Version) -> Version {
99    let mut next = version.0.clone();
100    next.minor += 1;
101    next.patch = 0;
102    next.pre = Prerelease::EMPTY;
103    next.build = BuildMetadata::EMPTY;
104    Version(next)
105}
106
107/// Returns the next major version.
108#[must_use]
109pub fn next_major(version: &Version) -> Version {
110    let mut next = version.0.clone();
111    next.major += 1;
112    next.minor = 0;
113    next.patch = 0;
114    next.pre = Prerelease::EMPTY;
115    next.build = BuildMetadata::EMPTY;
116    Version(next)
117}
118
119/// Compares two semantic versions.
120#[must_use]
121pub fn compare_versions(left: &Version, right: &Version) -> Ordering {
122    left.cmp(right)
123}
124
125#[cfg(test)]
126mod tests {
127    use std::cmp::Ordering;
128
129    use super::{
130        compare_versions, is_prerelease, next_major, next_minor, next_patch, parse_version,
131    };
132
133    #[test]
134    fn parses_versions() {
135        let version = parse_version("1.2.3").expect("version should parse");
136
137        assert_eq!(version.to_string(), "1.2.3");
138        assert_eq!(version.as_semver().major, 1);
139        assert!(parse_version("not-a-version").is_err());
140    }
141
142    #[test]
143    fn detects_prereleases() {
144        let stable = parse_version("1.2.3").expect("stable version should parse");
145        let prerelease = parse_version("1.2.3-alpha.1").expect("prerelease should parse");
146
147        assert!(!is_prerelease(&stable));
148        assert!(is_prerelease(&prerelease));
149    }
150
151    #[test]
152    fn bumps_versions() {
153        let version = parse_version("0.1.2-alpha.1+build.7").expect("version should parse");
154
155        assert_eq!(next_patch(&version).to_string(), "0.1.3");
156        assert_eq!(next_minor(&version).to_string(), "0.2.0");
157        assert_eq!(next_major(&version).to_string(), "1.0.0");
158    }
159
160    #[test]
161    fn compares_versions() {
162        let earlier = parse_version("0.1.0").expect("version should parse");
163        let later = parse_version("0.2.0").expect("version should parse");
164
165        assert_eq!(compare_versions(&earlier, &later), Ordering::Less);
166        assert_eq!(compare_versions(&later, &earlier), Ordering::Greater);
167        assert_eq!(compare_versions(&earlier, &earlier), Ordering::Equal);
168    }
169}