governor-application 2.0.3

Application use cases and machine contracts for cargo-governor
Documentation
//! Owners use cases.

use governor_core::traits::registry::Registry;
use governor_owners::{OwnersDiff, ResolvedOwners, resolve_owners, validate_not_empty};
use serde::Serialize;
use std::collections::HashMap;
use std::path::Path;

use crate::error::{ApplicationError, ApplicationResult};
use crate::ports::{OwnerPackage, OwnersWorkspace, WorkspacePort};

/// Input for showing owners.
#[derive(Debug, Clone)]
pub struct OwnersShowInput {
    /// Workspace root.
    pub workspace_path: String,
    /// Include every package.
    pub all: bool,
    /// Include owner source explanation.
    pub explain: bool,
}

/// Input for checking owners.
#[derive(Debug, Clone)]
pub struct OwnersCheckInput {
    /// Workspace root.
    pub workspace_path: String,
    /// Include every package.
    pub all: bool,
}

/// Input for syncing owners.
#[derive(Debug, Clone)]
pub struct OwnersSyncInput {
    /// Workspace root.
    pub workspace_path: String,
    /// Include every package.
    pub all: bool,
    /// Dry run.
    pub dry_run: bool,
}

/// Owner entry in output.
#[derive(Debug, Clone, Serialize)]
pub struct OwnerEntry {
    /// Login.
    pub owner: String,
    /// Source in merged config.
    pub source: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
pub struct OwnersDiffView {
    pub to_add: Vec<String>,
    pub to_remove: Vec<String>,
}

impl From<&OwnersDiff> for OwnersDiffView {
    fn from(value: &OwnersDiff) -> Self {
        Self {
            to_add: value.to_add.clone(),
            to_remove: value.to_remove.clone(),
        }
    }
}

/// Owners package report.
#[derive(Debug, Clone, Serialize)]
pub struct OwnersPackageReport {
    /// Crate name.
    pub name: String,
    /// Resolved owners.
    pub owners: Vec<OwnerEntry>,
    /// Total owner count.
    pub total: usize,
    /// Optional drift diff.
    pub diff: Option<OwnersDiffView>,
    /// Sync result if relevant.
    pub sync: Option<OwnersSyncStatus>,
}

/// Sync status for a package.
#[derive(Debug, Clone, Serialize)]
pub struct OwnersSyncStatus {
    /// Variant name.
    pub status: String,
    /// Applied or planned diff.
    pub diff: Option<OwnersDiffView>,
    /// Errors keyed by owner.
    pub errors: HashMap<String, String>,
}

/// Output for owners show.
#[derive(Debug, Clone, Serialize)]
pub struct OwnersShowOutput {
    /// Workspace root display.
    pub workspace: String,
    /// Packages shown.
    pub packages: Vec<OwnersPackageReport>,
}

/// Output for owners check.
#[derive(Debug, Clone, Serialize)]
pub struct OwnersCheckOutput {
    /// Workspace root display.
    pub workspace: String,
    /// Whether any drift was found.
    pub drift_detected: bool,
    /// Per-package reports.
    pub packages: Vec<OwnersPackageReport>,
}

/// Output for owners sync.
#[derive(Debug, Clone, Serialize)]
pub struct OwnersSyncOutput {
    /// Workspace root display.
    pub workspace: String,
    /// Whether any package had partial failures.
    pub partial_success: bool,
    /// Per-package sync status.
    pub packages: Vec<OwnersPackageReport>,
}

fn selected_packages(workspace: &OwnersWorkspace, all: bool) -> Vec<&OwnerPackage> {
    if all {
        workspace.packages.iter().collect()
    } else {
        workspace.packages.first().into_iter().collect()
    }
}

fn resolve_package(
    workspace: &OwnersWorkspace,
    pkg: &OwnerPackage,
) -> ApplicationResult<ResolvedOwners> {
    let workspace_config = workspace.workspace.clone().unwrap_or_default();
    let package_config = pkg.owners.clone().unwrap_or_default();
    let resolved = resolve_owners(&workspace_config, &package_config);
    validate_not_empty(&resolved.owners, &pkg.name)
        .map_err(|err| ApplicationError::InvalidArguments(err.to_string()))?;
    Ok(resolved)
}

fn owner_entries(resolved: &ResolvedOwners, explain: bool) -> Vec<OwnerEntry> {
    resolved
        .owners
        .iter()
        .map(|owner| OwnerEntry {
            owner: owner.clone(),
            source: explain
                .then(|| resolved.sources.get(owner).cloned())
                .flatten(),
        })
        .collect()
}

/// Execute owners show.
///
/// # Errors
///
/// Returns an error when the workspace metadata cannot be loaded or when
/// the resolved owners configuration is invalid.
pub async fn show<W: WorkspacePort>(
    workspace_port: &W,
    input: OwnersShowInput,
) -> ApplicationResult<OwnersShowOutput> {
    let workspace = workspace_port
        .load_owners_workspace(Path::new(&input.workspace_path))
        .await?;
    let reports = selected_packages(&workspace, input.all)
        .into_iter()
        .map(|pkg| {
            let resolved = resolve_package(&workspace, pkg)?;
            Ok(OwnersPackageReport {
                name: pkg.name.clone(),
                owners: owner_entries(&resolved, input.explain),
                total: resolved.len(),
                diff: None,
                sync: None,
            })
        })
        .collect::<ApplicationResult<Vec<_>>>()?;

    Ok(OwnersShowOutput {
        workspace: workspace.root.display().to_string(),
        packages: reports,
    })
}

/// Execute owners drift check.
///
/// # Errors
///
/// Returns an error when workspace metadata cannot be loaded or when the
/// registry lookup fails.
pub async fn check<W: WorkspacePort, R: Registry>(
    workspace_port: &W,
    registry: &R,
    input: OwnersCheckInput,
) -> ApplicationResult<OwnersCheckOutput> {
    let workspace = workspace_port
        .load_owners_workspace(Path::new(&input.workspace_path))
        .await?;
    let mut reports = Vec::new();
    let mut drift_detected = false;

    for pkg in selected_packages(&workspace, input.all) {
        let resolved = resolve_package(&workspace, pkg)?;
        let current = registry
            .list_owners(&pkg.name)
            .await?
            .into_iter()
            .map(|owner| owner.username)
            .collect::<Vec<_>>();
        let diff = OwnersDiff::calculate(&current, &resolved.owners);
        drift_detected |= !diff.is_empty();

        reports.push(OwnersPackageReport {
            name: pkg.name.clone(),
            owners: owner_entries(&resolved, true),
            total: resolved.len(),
            diff: Some(OwnersDiffView::from(&diff)),
            sync: None,
        });
    }

    Ok(OwnersCheckOutput {
        workspace: workspace.root.display().to_string(),
        drift_detected,
        packages: reports,
    })
}

/// Execute owners sync.
///
/// # Errors
///
/// Returns an error when workspace metadata cannot be loaded or when the
/// registry lookup for the current owner set fails.
pub async fn sync<W: WorkspacePort, R: Registry>(
    workspace_port: &W,
    registry: &R,
    input: OwnersSyncInput,
) -> ApplicationResult<OwnersSyncOutput> {
    let workspace = workspace_port
        .load_owners_workspace(Path::new(&input.workspace_path))
        .await?;
    let mut reports = Vec::new();
    let mut partial_success = false;

    for pkg in selected_packages(&workspace, input.all) {
        let resolved = resolve_package(&workspace, pkg)?;
        let current = registry
            .list_owners(&pkg.name)
            .await?
            .into_iter()
            .map(|owner| owner.username)
            .collect::<Vec<_>>();
        let diff = OwnersDiff::calculate(&current, &resolved.owners);

        let sync = if diff.is_empty() {
            OwnersSyncStatus {
                status: "already_in_sync".to_string(),
                diff: None,
                errors: HashMap::new(),
            }
        } else if input.dry_run {
            OwnersSyncStatus {
                status: "dry_run".to_string(),
                diff: Some(OwnersDiffView::from(&diff)),
                errors: HashMap::new(),
            }
        } else {
            let mut errors = HashMap::new();
            for owner in &diff.to_add {
                if let Err(error) = registry.add_owner(&pkg.name, owner).await {
                    errors.insert(owner.clone(), error.to_string());
                }
            }
            for owner in &diff.to_remove {
                if let Err(error) = registry.remove_owner(&pkg.name, owner).await {
                    errors.insert(owner.clone(), error.to_string());
                }
            }

            let status = if errors.is_empty() {
                "success"
            } else {
                partial_success = true;
                "partial"
            };

            OwnersSyncStatus {
                status: status.to_string(),
                diff: Some(OwnersDiffView::from(&diff)),
                errors,
            }
        };

        reports.push(OwnersPackageReport {
            name: pkg.name.clone(),
            owners: owner_entries(&resolved, true),
            total: resolved.len(),
            diff: Some(OwnersDiffView::from(&diff)),
            sync: Some(sync),
        });
    }

    Ok(OwnersSyncOutput {
        workspace: workspace.root.display().to_string(),
        partial_success,
        packages: reports,
    })
}