use crate::error::{Result, VersionError};
use semver::{Version, Prerelease, BuildMetadata};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum VersionBump {
Major,
Minor,
Patch,
Exact(Version),
}
#[derive(Debug, Clone)]
pub struct VersionBumper {
current_version: Version,
}
impl VersionBumper {
pub fn new(current_version: &str) -> Result<Self> {
let version = Version::from_str(current_version)
.map_err(|e| VersionError::ParseFailed {
version: current_version.to_string(),
source: e,
})?;
Ok(Self {
current_version: version,
})
}
pub fn from_version(version: Version) -> Self {
Self {
current_version: version,
}
}
pub fn bump(&self, bump_type: VersionBump) -> Result<Version> {
match bump_type {
VersionBump::Major => self.bump_major(),
VersionBump::Minor => self.bump_minor(),
VersionBump::Patch => self.bump_patch(),
VersionBump::Exact(version) => self.bump_exact(version),
}
}
fn bump_major(&self) -> Result<Version> {
let mut new_version = self.current_version.clone();
new_version.major += 1;
new_version.minor = 0;
new_version.patch = 0;
new_version.pre = Prerelease::EMPTY;
new_version.build = BuildMetadata::EMPTY;
Ok(new_version)
}
fn bump_minor(&self) -> Result<Version> {
let mut new_version = self.current_version.clone();
new_version.minor += 1;
new_version.patch = 0;
new_version.pre = Prerelease::EMPTY;
new_version.build = BuildMetadata::EMPTY;
Ok(new_version)
}
fn bump_patch(&self) -> Result<Version> {
let mut new_version = self.current_version.clone();
new_version.patch += 1;
new_version.pre = Prerelease::EMPTY;
new_version.build = BuildMetadata::EMPTY;
Ok(new_version)
}
fn bump_exact(&self, version: Version) -> Result<Version> {
self.validate_version_progression(&version)?;
Ok(version)
}
fn validate_version_progression(&self, new_version: &Version) -> Result<()> {
if new_version <= &self.current_version {
return Err(VersionError::InvalidVersion {
version: new_version.to_string(),
reason: format!(
"New version '{}' must be greater than current version '{}'",
new_version, self.current_version
),
}.into());
}
let major_increased = new_version.major > self.current_version.major;
let minor_increased = new_version.minor > self.current_version.minor;
let patch_increased = new_version.patch > self.current_version.patch;
if major_increased {
if new_version.minor != 0 || new_version.patch != 0 {
return Err(VersionError::InvalidVersion {
version: new_version.to_string(),
reason: "Major version bump should reset minor and patch to 0".to_string(),
}.into());
}
} else if minor_increased {
if new_version.patch != 0 {
return Err(VersionError::InvalidVersion {
version: new_version.to_string(),
reason: "Minor version bump should reset patch to 0".to_string(),
}.into());
}
} else if !patch_increased {
return Err(VersionError::InvalidVersion {
version: new_version.to_string(),
reason: "Version must increment at least one component".to_string(),
}.into());
}
Ok(())
}
pub fn current_version(&self) -> &Version {
&self.current_version
}
pub fn validate_version_string(version: &str) -> Result<Version> {
Version::from_str(version)
.map_err(|e| VersionError::ParseFailed {
version: version.to_string(),
source: e,
}.into())
}
pub fn calculate_bump_type(&self, target_version: &Version) -> Result<VersionBump> {
if target_version <= &self.current_version {
return Err(VersionError::InvalidVersion {
version: target_version.to_string(),
reason: "Target version must be greater than current version".to_string(),
}.into());
}
if target_version.major > self.current_version.major {
if target_version.minor == 0 && target_version.patch == 0 {
Ok(VersionBump::Major)
} else {
Ok(VersionBump::Exact(target_version.clone()))
}
} else if target_version.minor > self.current_version.minor {
if target_version.patch == 0 {
Ok(VersionBump::Minor)
} else {
Ok(VersionBump::Exact(target_version.clone()))
}
} else if target_version.patch > self.current_version.patch {
Ok(VersionBump::Patch)
} else {
Ok(VersionBump::Exact(target_version.clone()))
}
}
pub fn preview_bumps(&self) -> Result<BumpPreview> {
Ok(BumpPreview {
current: self.current_version.clone(),
major: self.bump_major()?,
minor: self.bump_minor()?,
patch: self.bump_patch()?,
})
}
pub fn has_prerelease_or_build(&self) -> bool {
!self.current_version.pre.is_empty() || !self.current_version.build.is_empty()
}
pub fn version_components(&self) -> (u64, u64, u64) {
(
self.current_version.major,
self.current_version.minor,
self.current_version.patch,
)
}
pub fn to_release_version(&self) -> Version {
let mut release_version = self.current_version.clone();
release_version.pre = Prerelease::EMPTY;
release_version.build = BuildMetadata::EMPTY;
release_version
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BumpPreview {
pub current: Version,
pub major: Version,
pub minor: Version,
pub patch: Version,
}
impl BumpPreview {
pub fn get_version<'a>(&'a self, bump_type: &'a VersionBump) -> Option<&'a Version> {
match bump_type {
VersionBump::Major => Some(&self.major),
VersionBump::Minor => Some(&self.minor),
VersionBump::Patch => Some(&self.patch),
VersionBump::Exact(version) => Some(version),
}
}
pub fn format_preview(&self) -> String {
format!(
"Current: {} | Major: {} | Minor: {} | Patch: {}",
self.current, self.major, self.minor, self.patch
)
}
}
impl FromStr for VersionBump {
type Err = VersionError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"major" => Ok(VersionBump::Major),
"minor" => Ok(VersionBump::Minor),
"patch" => Ok(VersionBump::Patch),
version_str => {
let version = Version::from_str(version_str)
.map_err(|e| VersionError::ParseFailed {
version: version_str.to_string(),
source: e,
})?;
Ok(VersionBump::Exact(version))
}
}
}
}
impl std::fmt::Display for VersionBump {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VersionBump::Major => write!(f, "major"),
VersionBump::Minor => write!(f, "minor"),
VersionBump::Patch => write!(f, "patch"),
VersionBump::Exact(version) => write!(f, "{}", version),
}
}
}
#[cfg(test)]
mod tests {
}