governor-core 1.3.0

Core domain and application logic for cargo-governor
Documentation
//! Version-related domain entities
//!
//! This module provides semantic versioning utilities for determining version bumps
//! and managing release versions according to Semantic Versioning 2.0.0.

use semver::{BuildMetadata, Prerelease, Version};
use serde::{Deserialize, Serialize};

/// Semantic version with additional metadata
///
/// Wraps a semver `Version` with convenient metadata for pre-release detection
/// and type identification.
///
/// # Examples
///
/// ```
/// use governor_core::domain::version::SemanticVersion;
///
/// let version = SemanticVersion::parse("1.2.3").unwrap();
/// assert_eq!(version.major(), 1);
/// assert_eq!(version.minor(), 2);
/// assert_eq!(version.patch(), 3);
/// assert!(!version.is_prerelease);
///
/// let prerelease = SemanticVersion::parse("1.2.3-beta.1").unwrap();
/// assert!(prerelease.is_prerelease);
/// assert_eq!(prerelease.prerelease_type, Some("beta".to_string()));
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SemanticVersion {
    /// The semantic version
    pub version: Version,
    /// Whether this is a pre-release version
    pub is_prerelease: bool,
    /// The prerelease identifier (e.g., "beta", "alpha")
    pub prerelease_type: Option<String>,
}

impl SemanticVersion {
    /// Create a new semantic version
    ///
    /// Automatically extracts prerelease metadata from the version.
    #[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,
        }
    }

    /// Parse from string
    ///
    /// Parses a version string according to Semantic Versioning 2.0.0.
    ///
    /// # Examples
    ///
    /// ```
    /// use governor_core::domain::version::SemanticVersion;
    ///
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let version = SemanticVersion::parse("1.2.3")?;
    /// assert_eq!(version.to_string(), "1.2.3");
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    ///
    /// Returns `VersionParseError` if the input string is not a valid semver version.
    /// This includes:
    /// - Malformed version strings (e.g., "1.2", "v1.2.3")
    /// - Invalid semver components (e.g., negative numbers)
    pub fn parse(s: &str) -> Result<Self, VersionParseError> {
        let version = Version::parse(s).map_err(VersionParseError)?;
        Ok(Self::new(version))
    }

    /// Get the major version
    #[must_use]
    pub const fn major(&self) -> u64 {
        self.version.major
    }

    /// Get the minor version
    #[must_use]
    pub const fn minor(&self) -> u64 {
        self.version.minor
    }

    /// Get the patch version
    #[must_use]
    pub const fn patch(&self) -> u64 {
        self.version.patch
    }

    /// Bump major version
    #[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)
    }

    /// Bump minor 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)
    }

    /// Bump patch 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)
    }

    /// Create a prerelease version
    ///
    /// # Panics
    ///
    /// Panics if the prerelease string is invalid according to semver spec
    #[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)
    }
}

/// Version bump type
///
/// Represents the type of version bump according to Semantic Versioning 2.0.0:
///
/// - **Major**: Incompatible API changes (`1.0.0` → `2.0.0`)
/// - **Minor**: New functionality (backwards compatible) (`1.0.0` → `1.1.0`)
/// - **Patch**: Bug fixes (backwards compatible) (`1.0.0` → `1.0.1`)
/// - **None**: No version bump needed
///
/// # Examples
///
/// ```
/// use governor_core::domain::version::{BumpType, SemanticVersion};
///
/// let version = SemanticVersion::parse("1.2.3").unwrap();
/// assert_eq!(BumpType::Major.apply_to(&version).to_string(), "2.0.0");
/// assert_eq!(BumpType::Minor.apply_to(&version).to_string(), "1.3.0");
/// assert_eq!(BumpType::Patch.apply_to(&version).to_string(), "1.2.4");
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BumpType {
    /// Major version bump (breaking changes)
    Major,
    /// Minor version bump (new features)
    Minor,
    /// Patch version bump (bug fixes)
    Patch,
    /// No bump needed
    None,
}

impl BumpType {
    /// Apply this bump to a version
    #[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(),
        }
    }
}

/// Version recommendation from analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionRecommendation {
    /// The current version
    pub current: SemanticVersion,
    /// The recommended bump type
    pub bump: BumpType,
    /// The recommended new version
    pub recommended: SemanticVersion,
    /// Confidence in this recommendation (0.0 to 1.0)
    pub confidence: f64,
    /// Reasoning for this recommendation
    pub reasoning: String,
    /// Breaking changes detected
    pub breaking_changes: Vec<BreakingChange>,
    /// Features detected
    pub features: Vec<Feature>,
    /// Fixes detected
    pub fixes: Vec<Fix>,
}

/// A breaking change detected in the commit history
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BreakingChange {
    /// Commit hash
    pub commit_hash: String,
    /// Short commit hash
    pub short_hash: String,
    /// Commit message
    pub message: String,
    /// Description of the breaking change
    pub breaking_description: String,
    /// Affected crates
    pub affected_crates: Vec<String>,
    /// Migration complexity
    pub migration_complexity: MigrationComplexity,
}

/// Migration complexity for a breaking change
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MigrationComplexity {
    /// Simple migration (e.g., rename)
    Simple,
    /// Medium migration (e.g., API signature change)
    Medium,
    /// Complex migration (e.g., architectural change)
    Complex,
}

/// A feature detected in the commit history
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Feature {
    /// Commit hash
    pub commit_hash: String,
    /// Short commit hash
    pub short_hash: String,
    /// Commit message
    pub message: String,
    /// Scope of the feature
    pub scope: Option<String>,
    /// Affected crates
    pub affected_crates: Vec<String>,
}

/// A fix detected in the commit history
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fix {
    /// Commit hash
    pub commit_hash: String,
    /// Short commit hash
    pub short_hash: String,
    /// Commit message
    pub message: String,
    /// Scope of the fix
    pub scope: Option<String>,
    /// Affected crates
    pub affected_crates: Vec<String>,
}

/// Version parse error
#[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");
    }
}