governor-core 1.11.0

Core domain and application logic for cargo-governor
Documentation
//! Registry trait (crates.io abstraction)
//!
//! This module provides a trait abstraction for interacting with package registries
//! such as [crates.io](https://crates.io/).

use async_trait::async_trait;

use crate::domain::version::SemanticVersion;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Error type for registry operations
#[derive(Debug, thiserror::Error)]
pub enum RegistryError {
    /// Crate not found on the registry
    #[error("Crate not found: {0}")]
    NotFound(String),

    /// Specific version not found
    #[error("Version not found: {0}")]
    VersionNotFound(String),

    /// API request failed
    #[error("API request failed: {0}")]
    ApiError(String),

    /// Network error
    #[error("Network error: {0}")]
    NetworkError(String),

    /// Authentication failed
    #[error("Authentication failed")]
    AuthFailed,

    /// Version already published
    #[error("Version already published: {0}@{1}")]
    AlreadyPublished(String, SemanticVersion),

    /// IO error
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    /// Serialization error
    #[error("Serialization error: {0}")]
    SerializationError(String),
}

/// Trait for registry operations (crates.io, alternative registries)
///
/// Provides an abstraction over package registry operations needed for
/// publishing and managing Rust crates.
///
/// # Examples
///
/// The trait is used by implementors that provide registry operations:
///
/// ```text
/// use governor_core::traits::registry::Registry;
///
/// // A concrete implementation would provide:
/// let info = registry.get_crate("serde").await?.unwrap();
/// println!("Latest version: {}", info.latest_version);
/// ```
#[async_trait]
pub trait Registry: Send + Sync {
    /// Get the name of this registry
    fn name(&self) -> &str;

    /// Get crate information
    ///
    /// # Arguments
    ///
    /// * `name` - Crate name
    ///
    /// # Returns
    ///
    /// `Some(CrateInfo)` if the crate exists, `None` otherwise.
    async fn get_crate(&self, name: &str) -> Result<Option<CrateInfo>, RegistryError>;

    /// Check if a version is published
    ///
    /// # Arguments
    ///
    /// * `name` - Crate name
    /// * `version` - Version to check
    ///
    /// # Returns
    ///
    /// `true` if the version is published, `false` otherwise.
    async fn is_published(
        &self,
        name: &str,
        version: &SemanticVersion,
    ) -> Result<bool, RegistryError>;

    /// Publish a crate
    ///
    /// # Arguments
    ///
    /// * `crate_data` - Package information and file paths
    ///
    /// # Returns
    ///
    /// `PublishResult` containing the outcome of the publish operation.
    ///
    /// # Errors
    ///
    /// Returns `RegistryError::AlreadyPublished` if this version already exists.
    async fn publish(&self, crate_data: &CratePackage) -> Result<PublishResult, RegistryError>;

    /// Get download statistics
    ///
    /// # Arguments
    ///
    /// * `name` - Crate name
    /// * `version` - Version to query
    async fn get_download_stats(
        &self,
        name: &str,
        version: &SemanticVersion,
    ) -> Result<DownloadStats, RegistryError>;

    /// List crate owners
    ///
    /// # Arguments
    ///
    /// * `name` - Crate name
    async fn list_owners(&self, name: &str) -> Result<Vec<CrateOwner>, RegistryError>;

    /// Add a crate owner
    ///
    /// # Errors
    ///
    /// Returns `RegistryError::NotFound` if the crate doesn't exist.
    async fn add_owner(&self, name: &str, username: &str) -> Result<(), RegistryError>;

    /// Remove a crate owner
    ///
    /// # Errors
    ///
    /// Returns `RegistryError::NotFound` if the crate doesn't exist.
    async fn remove_owner(&self, name: &str, username: &str) -> Result<(), RegistryError>;

    /// Get crate metadata
    ///
    /// # Arguments
    ///
    /// * `name` - Crate name
    async fn get_metadata(&self, name: &str) -> Result<Option<CrateMetadata>, RegistryError>;
}

/// Information about a published crate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateInfo {
    /// Crate name
    pub name: String,
    /// Latest version
    pub latest_version: SemanticVersion,
    /// All published versions
    pub versions: Vec<SemanticVersion>,
    /// Total downloads
    pub downloads: u64,
    /// Crate description
    pub description: Option<String>,
    /// Repository URL
    pub repository: Option<String>,
    /// Homepage URL
    pub homepage: Option<String>,
    /// Documentation URL
    pub documentation: Option<String>,
    /// When the crate was first published
    pub created_at: DateTime<Utc>,
    /// When the crate was last updated
    pub updated_at: DateTime<Utc>,
    /// Number of downloads in the last 90 days
    pub recent_downloads: Option<u64>,
}

/// A crate package ready to publish
#[derive(Debug, Clone)]
pub struct CratePackage {
    /// Crate name
    pub name: String,
    /// Version
    pub version: SemanticVersion,
    /// Path to the prepared .crate file
    pub crate_file: std::path::PathBuf,
    /// Path to Cargo.toml
    pub manifest_path: std::path::PathBuf,
    /// Registry token
    pub token: String,
    /// Whether this is a dry run
    pub dry_run: bool,
}

/// Result of a publish operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishResult {
    /// Whether the publish was successful
    pub success: bool,
    /// Crate name
    pub crate_name: String,
    /// Published version
    pub version: SemanticVersion,
    /// Time taken to publish
    pub duration_ms: u64,
    /// crates.io URL
    pub crates_io_url: String,
    /// Any warnings
    pub warnings: Vec<String>,
}

/// Download statistics for a crate version
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DownloadStats {
    /// Crate name
    pub crate_name: String,
    /// Version
    pub version: SemanticVersion,
    /// Total downloads
    pub total: u64,
    /// Downloads in the last 90 days
    pub recent_downloads: u64,
    /// Downloads by date (last 90 days)
    pub daily_downloads: Vec<DailyDownloads>,
}

/// Daily download count
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DailyDownloads {
    /// Date
    pub date: DateTime<Utc>,
    /// Download count
    pub downloads: u64,
}

/// A crate owner
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateOwner {
    /// GitHub username
    pub username: String,
    /// Display name
    pub name: Option<String>,
    /// Avatar URL
    pub avatar: Option<String>,
    /// URL to profile
    pub url: String,
}

/// Extended crate metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateMetadata {
    /// Crate name
    pub name: String,
    /// All versions
    pub versions: Vec<VersionMetadata>,
    /// Crate keywords
    pub keywords: Vec<String>,
    /// Crate categories
    pub categories: Vec<String>,
    /// Crate badges
    pub badges: Vec<Badge>,
}

/// Metadata for a specific version
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionMetadata {
    /// Version number
    pub version: SemanticVersion,
    /// Checksum (SHA256)
    pub checksum: String,
    /// Path to .crate file
    pub dl_path: String,
    /// When this version was published
    pub published_at: DateTime<Utc>,
    /// Features for this version
    pub features: std::collections::HashMap<String, Vec<String>>,
    /// Dependencies for this version
    pub dependencies: Vec<DependencyInfo>,
    /// Whether this is a yanked version
    pub yanked: bool,
}

/// Dependency information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyInfo {
    /// Crate name
    pub name: String,
    /// Version requirement
    pub req: String,
    /// Whether this is a dev dependency
    pub dev: bool,
    /// Whether this is a build dependency
    pub build: bool,
    /// Optional dependency
    pub optional: bool,
    /// Features used
    pub features: Vec<String>,
    /// Target (if any)
    pub target: Option<String>,
    /// Rename (if using `dep:` syntax)
    pub rename: Option<String>,
}

/// A badge (displayed on crates.io)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Badge {
    /// Badge type
    pub badge_type: String,
    /// Badge data
    pub attributes: std::collections::HashMap<String, String>,
}

/// Registry configuration
#[derive(Debug, Clone)]
pub struct RegistryConfig {
    /// Registry name
    pub name: String,
    /// API base URL
    pub api_url: String,
    /// Upload URL
    pub upload_url: String,
    /// Authentication token
    pub token: Option<String>,
    /// Whether to use SSH for git operations
    pub use_ssh: bool,
}

impl Default for RegistryConfig {
    fn default() -> Self {
        Self {
            name: "crates.io".to_string(),
            api_url: "https://crates.io/api/v1".to_string(),
            upload_url: "https://crates.io".to_string(),
            token: None,
            use_ssh: false,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_registry_config_default() {
        let config = RegistryConfig::default();
        assert_eq!(config.name, "crates.io");
        assert_eq!(config.api_url, "https://crates.io/api/v1");
    }

    #[test]
    fn test_publish_result_serialization() {
        let result = PublishResult {
            success: true,
            crate_name: "test-crate".to_string(),
            version: SemanticVersion::parse("1.0.0").unwrap(),
            duration_ms: 1000,
            crates_io_url: "https://crates.io/crates/test-crate/1.0.0".to_string(),
            warnings: Vec::new(),
        };
        let json = serde_json::to_string(&result).unwrap();
        assert!(json.contains("test-crate"));
    }
}