auths-sdk 0.1.2

Application services layer for Auths identity operations
Documentation
//! Diagnostic provider ports for system health checks.
//!
//! Follows Interface Segregation: one trait per tool category so tests
//! only mock what they need.

use serde::{Deserialize, Serialize};

/// Category of a diagnostic check — determines exit code behavior.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CheckCategory {
    /// Critical: if this check fails, Auths is non-functional.
    /// Doctor should exit 1 if any Critical check fails.
    Critical,
    /// Advisory: if this check fails, Auths works but environment is not ideal.
    /// Doctor exits 2 if all Critical checks pass but some Advisory checks fail.
    Advisory,
}

/// A structured issue found during git signing configuration checks.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ConfigIssue {
    /// A key exists but has the wrong value.
    Mismatch {
        /// The git config key name.
        key: String,
        /// The expected value.
        expected: String,
        /// The actual value found.
        actual: String,
    },
    /// A required key is not set.
    Absent(String),
}

/// Result of a single diagnostic check.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResult {
    /// Human-readable name of the diagnostic check.
    pub name: String,
    /// Whether the check passed.
    pub passed: bool,
    /// Free-form informational text (e.g. version strings).
    pub message: Option<String>,
    /// Structured config issues — populated only by config checks.
    #[serde(default)]
    pub config_issues: Vec<ConfigIssue>,
    /// Category of this check (Critical vs Advisory).
    /// Critical checks failing means Auths is non-functional.
    /// Advisory checks failing means environment is suboptimal.
    #[serde(default = "default_check_category")]
    pub category: CheckCategory,
}

fn default_check_category() -> CheckCategory {
    CheckCategory::Advisory
}

/// Aggregated diagnostic report.
#[derive(Debug, Serialize, Deserialize)]
pub struct DiagnosticReport {
    /// All individual check results in the report.
    pub checks: Vec<CheckResult>,
}

/// Errors from diagnostic check execution.
#[derive(Debug, thiserror::Error)]
pub enum DiagnosticError {
    /// A diagnostic check failed to execute.
    #[error("check failed to execute: {0}")]
    ExecutionFailed(String),

    /// The requested check name does not exist.
    #[error("check not found: {0}")]
    CheckNotFound(String),
}

/// Port for Git-related diagnostic checks.
///
/// Usage:
/// ```ignore
/// let result = provider.check_git_version()?;
/// let config_val = provider.get_git_config("user.email")?;
/// ```
pub trait GitDiagnosticProvider: Send + Sync {
    /// Check that git is installed and return version info.
    fn check_git_version(&self) -> Result<CheckResult, DiagnosticError>;

    /// Read a global git config value, returning `None` if unset.
    fn get_git_config(&self, key: &str) -> Result<Option<String>, DiagnosticError>;
}

/// Port for cryptographic tool diagnostic checks.
///
/// Usage:
/// ```ignore
/// let result = provider.check_ssh_keygen_available()?;
/// ```
pub trait CryptoDiagnosticProvider: Send + Sync {
    /// Check that ssh-keygen is available on the system.
    fn check_ssh_keygen_available(&self) -> Result<CheckResult, DiagnosticError>;

    /// Return the raw SSH version string from `ssh -V`.
    ///
    /// Default returns "unknown" so existing implementors are not broken.
    fn check_ssh_version(&self) -> Result<String, DiagnosticError> {
        Ok("unknown".to_string())
    }
}

impl<T: GitDiagnosticProvider> GitDiagnosticProvider for &T {
    fn check_git_version(&self) -> Result<CheckResult, DiagnosticError> {
        (**self).check_git_version()
    }
    fn get_git_config(&self, key: &str) -> Result<Option<String>, DiagnosticError> {
        (**self).get_git_config(key)
    }
}

/// A fix that can be applied to resolve a failed diagnostic check.
///
/// Usage:
/// ```ignore
/// if fix.can_fix(&check) {
///     if fix.is_safe() || user_confirmed() {
///         let message = fix.apply()?;
///     }
/// }
/// ```
pub trait DiagnosticFix: Send + Sync {
    /// Human-readable name of this fix.
    fn name(&self) -> &str;

    /// Whether this fix is safe to apply without user confirmation.
    fn is_safe(&self) -> bool;

    /// Whether this fix can address the given failed check.
    fn can_fix(&self, check: &CheckResult) -> bool;

    /// Apply the fix, returning a description of what was done.
    fn apply(&self) -> Result<String, DiagnosticError>;
}

/// Record of a fix that was applied.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixApplied {
    /// Name of the fix.
    pub name: String,
    /// Description of what was done.
    pub message: String,
}

impl<T: CryptoDiagnosticProvider> CryptoDiagnosticProvider for &T {
    fn check_ssh_keygen_available(&self) -> Result<CheckResult, DiagnosticError> {
        (**self).check_ssh_keygen_available()
    }
    fn check_ssh_version(&self) -> Result<String, DiagnosticError> {
        (**self).check_ssh_version()
    }
}