use semver::{BuildMetadata, Prerelease, Version};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SemanticVersion {
pub version: Version,
pub is_prerelease: bool,
pub prerelease_type: Option<String>,
}
impl SemanticVersion {
#[must_use]
pub fn new(version: Version) -> Self {
let is_prerelease = !version.pre.is_empty();
let prerelease_type = if is_prerelease {
version.pre.split('.').next().map(String::from)
} else {
None
};
Self {
version,
is_prerelease,
prerelease_type,
}
}
pub fn parse(s: &str) -> Result<Self, VersionParseError> {
let version = Version::parse(s).map_err(VersionParseError)?;
Ok(Self::new(version))
}
#[must_use]
pub const fn major(&self) -> u64 {
self.version.major
}
#[must_use]
pub const fn minor(&self) -> u64 {
self.version.minor
}
#[must_use]
pub const fn patch(&self) -> u64 {
self.version.patch
}
#[must_use]
pub fn bump_major(&self) -> Self {
let mut new_version = self.version.clone();
new_version.major += 1;
new_version.minor = 0;
new_version.patch = 0;
new_version.pre = Prerelease::EMPTY;
new_version.build = BuildMetadata::EMPTY;
Self::new(new_version)
}
#[must_use]
pub fn bump_minor(&self) -> Self {
let mut new_version = self.version.clone();
new_version.minor += 1;
new_version.patch = 0;
new_version.pre = Prerelease::EMPTY;
new_version.build = BuildMetadata::EMPTY;
Self::new(new_version)
}
#[must_use]
pub fn bump_patch(&self) -> Self {
let mut new_version = self.version.clone();
new_version.patch += 1;
new_version.pre = Prerelease::EMPTY;
new_version.build = BuildMetadata::EMPTY;
Self::new(new_version)
}
#[must_use]
pub fn with_prerelease(&self, prerelease: &str) -> Self {
let mut new_version = self.version.clone();
new_version.pre = Prerelease::new(prerelease).expect("invalid prerelease");
Self::new(new_version)
}
}
impl From<Version> for SemanticVersion {
fn from(version: Version) -> Self {
Self::new(version)
}
}
impl From<SemanticVersion> for Version {
fn from(sv: SemanticVersion) -> Self {
sv.version
}
}
impl std::fmt::Display for SemanticVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.version)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BumpType {
Major,
Minor,
Patch,
None,
}
impl BumpType {
#[must_use]
pub fn apply_to(self, version: &SemanticVersion) -> SemanticVersion {
match self {
Self::Major => version.bump_major(),
Self::Minor => version.bump_minor(),
Self::Patch => version.bump_patch(),
Self::None => version.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionRecommendation {
pub current: SemanticVersion,
pub bump: BumpType,
pub recommended: SemanticVersion,
pub confidence: f64,
pub reasoning: String,
pub breaking_changes: Vec<BreakingChange>,
pub features: Vec<Feature>,
pub fixes: Vec<Fix>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BreakingChange {
pub commit_hash: String,
pub short_hash: String,
pub message: String,
pub breaking_description: String,
pub affected_crates: Vec<String>,
pub migration_complexity: MigrationComplexity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MigrationComplexity {
Simple,
Medium,
Complex,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Feature {
pub commit_hash: String,
pub short_hash: String,
pub message: String,
pub scope: Option<String>,
pub affected_crates: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fix {
pub commit_hash: String,
pub short_hash: String,
pub message: String,
pub scope: Option<String>,
pub affected_crates: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
#[error("Failed to parse version: {0}")]
pub struct VersionParseError(#[from] semver::Error);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_version() {
let v = SemanticVersion::parse("1.2.3").unwrap();
assert_eq!(v.major(), 1);
assert_eq!(v.minor(), 2);
assert_eq!(v.patch(), 3);
assert!(!v.is_prerelease);
}
#[test]
fn test_parse_prerelease() {
let v = SemanticVersion::parse("1.2.3-beta.1").unwrap();
assert!(v.is_prerelease);
assert_eq!(v.prerelease_type, Some("beta".to_string()));
}
#[test]
fn test_bump_major() {
let v = SemanticVersion::parse("1.2.3").unwrap();
let bumped = v.bump_major();
assert_eq!(bumped.to_string(), "2.0.0");
}
#[test]
fn test_bump_minor() {
let v = SemanticVersion::parse("1.2.3").unwrap();
let bumped = v.bump_minor();
assert_eq!(bumped.to_string(), "1.3.0");
}
#[test]
fn test_bump_patch() {
let v = SemanticVersion::parse("1.2.3").unwrap();
let bumped = v.bump_patch();
assert_eq!(bumped.to_string(), "1.2.4");
}
#[test]
fn test_bump_type_apply() {
let v = SemanticVersion::parse("1.2.3").unwrap();
assert_eq!(BumpType::Major.apply_to(&v).to_string(), "2.0.0");
assert_eq!(BumpType::Minor.apply_to(&v).to_string(), "1.3.0");
assert_eq!(BumpType::Patch.apply_to(&v).to_string(), "1.2.4");
assert_eq!(BumpType::None.apply_to(&v).to_string(), "1.2.3");
}
}