cargo-governor 2.0.3

Machine-First, LLM-Ready, CI/CD-Native release automation tool for Rust crates
Documentation
//! Cargo workspace metadata parsing for owners configuration

use crate::error::{Error, Result};
use cargo_metadata::{Metadata, MetadataCommand};
use governor_owners::{PackageOwnersConfig, WorkspaceOwnersConfig};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;

#[derive(Debug, Deserialize)]
struct GovernorMetadata {
    owners: Option<OwnersMetadata>,
}

#[derive(Debug, Deserialize)]
struct OwnersMetadata {
    #[serde(default)]
    users: Vec<String>,
    #[serde(default)]
    groups: HashMap<String, Vec<String>>,
}

#[derive(Debug, Clone)]
pub struct CargoConfig {
    pub workspace: Option<WorkspaceOwnersConfig>,
    pub packages: Vec<PackageConfig>,
}

#[derive(Debug, Clone)]
pub struct PackageConfig {
    pub name: String,
    pub owners: Option<PackageOwnersConfig>,
}

impl CargoConfig {
    /// Loads configuration from the current Cargo workspace.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - Cargo metadata cannot be read
    /// - TOML parsing fails
    /// - Invalid configuration format
    pub fn from_current_workspace() -> Result<Self> {
        let metadata = MetadataCommand::new()
            .no_deps()
            .exec()
            .map_err(|e| Error::Config(format!("Failed to read cargo metadata: {e}")))?;

        Self::from_metadata(&metadata)
    }

    fn from_metadata(metadata: &Metadata) -> Result<Self> {
        // Parse workspace-level owners from metadata.workspace_metadata
        let workspace_config = metadata.workspace_metadata.as_object().and_then(|ws_meta| {
            ws_meta.get("governor").and_then(|g| {
                g.get("owners").and_then(|o| {
                    serde_json::from_value::<OwnersMetadata>(o.clone())
                        .ok()
                        .map(|owners| WorkspaceOwnersConfig {
                            users: owners.users,
                            groups: owners.groups,
                        })
                })
            })
        });

        let mut packages = Vec::new();

        for package in metadata.workspace_packages() {
            let name = package.name.clone();
            let owners = parse_package_owners(package.manifest_path.as_std_path())?;

            packages.push(PackageConfig { name, owners });
        }

        Ok(Self {
            workspace: workspace_config,
            packages,
        })
    }

    #[must_use]
    pub fn current_package(&self) -> Option<&PackageConfig> {
        self.packages.first()
    }
}

fn parse_owners_from_toml(content: &str) -> Result<Option<GovernorMetadata>> {
    let value: toml::Value =
        toml::from_str(content).map_err(|e| Error::Config(format!("Failed to parse TOML: {e}")))?;

    let governor = value
        .get("package")
        .and_then(|p| p.get("metadata"))
        .and_then(|m| m.get("governor"));

    governor
        .map(|v| {
            v.clone()
                .try_into()
                .map_err(|e| Error::Config(format!("Failed to parse governor metadata: {e}")))
        })
        .transpose()
}

fn parse_package_owners(path: &Path) -> Result<Option<PackageOwnersConfig>> {
    let content = std::fs::read_to_string(path)?;

    let metadata = parse_owners_from_toml(&content)?;

    Ok(metadata.and_then(|m| {
        m.owners.map(|o| PackageOwnersConfig {
            users: o.users,
            groups: o.groups.into_keys().collect(),
            ..Default::default()
        })
    }))
}