governor-core 1.10.1

Core domain and application logic for cargo-governor
Documentation
//! Dependency domain entity

use serde::{Deserialize, Serialize};

/// A dependency between crates in a workspace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceDependency {
    /// The dependent crate name
    pub from: String,
    /// The dependency crate name
    pub to: String,
    /// Version requirement
    pub version_req: String,
    /// Whether this is a dev dependency
    pub is_dev: bool,
    /// Whether this is a build dependency
    pub is_build: bool,
}

impl WorkspaceDependency {
    /// Create a new workspace dependency
    #[must_use]
    pub const fn new(from: String, to: String, version_req: String) -> Self {
        Self {
            from,
            to,
            version_req,
            is_dev: false,
            is_build: false,
        }
    }

    /// Get the dependency as a tuple
    #[must_use]
    pub fn as_tuple(&self) -> (&str, &str) {
        (&self.from, &self.to)
    }
}

/// A dependency graph for workspace crates
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyGraph {
    /// All dependencies
    pub dependencies: Vec<WorkspaceDependency>,
}

impl DependencyGraph {
    /// Create a new empty dependency graph
    #[must_use]
    pub const fn new() -> Self {
        Self {
            dependencies: Vec::new(),
        }
    }

    /// Add a dependency
    pub fn add(&mut self, dep: WorkspaceDependency) {
        self.dependencies.push(dep);
    }

    /// Get dependencies for a specific crate
    #[must_use]
    pub fn dependencies_for(&self, crate_name: &str) -> Vec<&WorkspaceDependency> {
        self.dependencies
            .iter()
            .filter(|d| d.from == crate_name)
            .collect()
    }

    /// Get crates that depend on a specific crate
    #[must_use]
    pub fn dependents_of(&self, crate_name: &str) -> Vec<&WorkspaceDependency> {
        self.dependencies
            .iter()
            .filter(|d| d.to == crate_name)
            .collect()
    }

    /// Check if there's a dependency between two crates
    #[must_use]
    pub fn has_dependency(&self, from: &str, to: &str) -> bool {
        self.dependencies
            .iter()
            .any(|d| d.from == from && d.to == to)
    }

    /// Get all crate names
    #[must_use]
    pub fn all_crates(&self) -> std::collections::HashSet<String> {
        let mut crates = std::collections::HashSet::new();
        for dep in &self.dependencies {
            crates.insert(dep.from.clone());
            crates.insert(dep.to.clone());
        }
        crates
    }

    /// Check for cycles
    #[must_use]
    pub fn has_cycles(&self) -> bool {
        use petgraph::algo::is_cyclic_directed;
        use petgraph::graph::DiGraph;

        let mut graph = DiGraph::new();
        let mut indices = std::collections::HashMap::new();

        // Add nodes
        for name in self.all_crates() {
            let idx = graph.add_node(name.clone());
            indices.insert(name, idx);
        }

        // Add edges
        for dep in &self.dependencies {
            if let (Some(&from), Some(&to)) = (indices.get(&dep.from), indices.get(&dep.to)) {
                graph.add_edge(from, to, ());
            }
        }

        is_cyclic_directed(&graph)
    }

    /// Calculate publish order (topological sort)
    ///
    /// # Errors
    ///
    /// Returns `CycleError` if a dependency cycle is detected in the graph
    pub fn publish_order(&self) -> Result<Vec<String>, CycleError> {
        use petgraph::algo::toposort;
        use petgraph::graph::DiGraph;

        let mut graph = DiGraph::new();
        let mut indices = std::collections::HashMap::new();

        // Add nodes
        for name in self.all_crates() {
            let idx = graph.add_node(name.clone());
            indices.insert(name, idx);
        }

        // Add edges (dependencies)
        for dep in &self.dependencies {
            if let (Some(&from), Some(&to)) = (indices.get(&dep.from), indices.get(&dep.to)) {
                // to must be published before from
                graph.add_edge(to, from, ());
            }
        }

        toposort(&graph, None).map_or(Err(CycleError), |order| {
            let mut crates = Vec::new();
            for idx in order {
                if let Some(name) = graph.node_weight(idx) {
                    crates.push(name.clone());
                }
            }
            Ok(crates)
        })
    }
}

impl Default for DependencyGraph {
    fn default() -> Self {
        Self::new()
    }
}

/// Error when a dependency cycle is detected
#[derive(Debug, Clone, Copy, thiserror::Error)]
#[error("Dependency cycle detected")]
pub struct CycleError;

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

    #[test]
    fn test_dependency_graph() {
        let mut graph = DependencyGraph::new();
        graph.add(WorkspaceDependency::new(
            "crate-a".to_string(),
            "crate-b".to_string(),
            "^1.0".to_string(),
        ));
        graph.add(WorkspaceDependency::new(
            "crate-b".to_string(),
            "crate-c".to_string(),
            "^1.0".to_string(),
        ));

        assert!(graph.has_dependency("crate-a", "crate-b"));
        assert!(!graph.has_dependency("crate-b", "crate-a"));

        let deps_a = graph.dependencies_for("crate-a");
        assert_eq!(deps_a.len(), 1);
        assert_eq!(deps_a[0].to, "crate-b");

        let deps_of_c = graph.dependents_of("crate-c");
        assert_eq!(deps_of_c.len(), 1);
        assert_eq!(deps_of_c[0].from, "crate-b");
    }

    #[test]
    fn test_publish_order() {
        let mut graph = DependencyGraph::new();
        graph.add(WorkspaceDependency::new(
            "crate-a".to_string(),
            "crate-b".to_string(),
            "^1.0".to_string(),
        ));
        graph.add(WorkspaceDependency::new(
            "crate-b".to_string(),
            "crate-c".to_string(),
            "^1.0".to_string(),
        ));

        let order = graph.publish_order().unwrap();
        // c must be before b, b must be before a
        assert_eq!(order, vec!["crate-c", "crate-b", "crate-a"]);
    }

    #[test]
    fn test_cycle_detection() {
        let mut graph = DependencyGraph::new();
        graph.add(WorkspaceDependency::new(
            "crate-a".to_string(),
            "crate-b".to_string(),
            "^1.0".to_string(),
        ));
        graph.add(WorkspaceDependency::new(
            "crate-b".to_string(),
            "crate-c".to_string(),
            "^1.0".to_string(),
        ));
        graph.add(WorkspaceDependency::new(
            "crate-c".to_string(),
            "crate-a".to_string(),
            "^1.0".to_string(),
        ));

        assert!(graph.has_cycles());
        assert!(graph.publish_order().is_err());
    }

    #[test]
    fn test_no_cycles() {
        let mut graph = DependencyGraph::new();
        graph.add(WorkspaceDependency::new(
            "crate-a".to_string(),
            "crate-b".to_string(),
            "^1.0".to_string(),
        ));
        graph.add(WorkspaceDependency::new(
            "crate-b".to_string(),
            "crate-c".to_string(),
            "^1.0".to_string(),
        ));

        assert!(!graph.has_cycles());
    }
}