bounded_graph 0.3.0

A thin newtype wrapper for `petgraph` to assist in the creation of graphs with restrictions on their edges
Documentation
use crate::{
    BoundedGraph, EdgeBounds, ImmutableEdgeBounds, SimpleEdgeBounds, SymmetricFixedEdgeCount,
};
use petgraph::data::DataMapMut;
use petgraph::graph::DefaultIx;

// Custom node where edge limits could be mutated (UNSAFE!)
// The edge limits are stored in a mutable field, making this unsafe for DataMapMut
#[derive(Debug, Clone)]
struct MutableLimitNode {
    max_edges: usize, // Mutable! This is the danger
    data: String,     // Other mutable data
}

impl EdgeBounds for MutableLimitNode {
    fn max_incoming_edges(&self) -> DefaultIx {
        self.max_edges as DefaultIx
    }

    fn max_outgoing_edges(&self) -> DefaultIx {
        self.max_edges as DefaultIx
    }
}

impl SimpleEdgeBounds for MutableLimitNode {}

#[test]
fn test_symmetric_fixed_edge_count_mutation_safety() {
    // SymmetricFixedEdgeCount - mutating data shouldn't affect limits
    let mut graph = BoundedGraph::<SymmetricFixedEdgeCount<2, i32>, ()>::new();
    let n1 = graph.add_node(SymmetricFixedEdgeCount::new(100));
    let n2 = graph.add_node(SymmetricFixedEdgeCount::new(200));

    assert_eq!(graph[n1].data, 100);
    assert_eq!(graph[n2].data, 200);

    // Add edges
    assert!(graph.add_edge(n1, n2, ()).is_ok());
    assert!(graph.add_edge(n1, n2, ()).is_ok());

    // Try to add a third edge (should fail)
    let result = graph.add_edge(n1, n2, ());
    assert!(result.is_err());

    // Mutate the data field using DataMapMut trait
    if let Some(node) = DataMapMut::node_weight_mut(&mut graph, n1) {
        node.data = 999;
    }

    assert_eq!(graph[n1].data, 999);

    // Try adding edge again - should still fail because MAX is const
    let result = graph.add_edge(n1, n2, ());
    assert!(
        result.is_err(),
        "SymmetricFixedEdgeCount is SAFE: Mutation didn't break limits!"
    );
}

#[test]
fn test_mutable_limit_node_protected() {
    // Custom node with mutable limits - cannot use DataMapMut
    let mut graph = BoundedGraph::<MutableLimitNode, ()>::new();
    let n1 = graph.add_node(MutableLimitNode {
        max_edges: 2,
        data: "Node1".to_string(),
    });
    let n2 = graph.add_node(MutableLimitNode {
        max_edges: 2,
        data: "Node2".to_string(),
    });

    // Verify initial data - demonstrate the fields are accessible
    assert_eq!(graph[n1].data, "Node1");
    assert_eq!(graph[n2].data, "Node2");
    assert_eq!(graph[n1].max_edges, 2);

    // Add edges up to limit
    assert!(graph.add_edge(n1, n2, ()).is_ok());
    assert!(graph.add_edge(n1, n2, ()).is_ok());

    // Should fail at capacity
    let result = graph.add_edge(n1, n2, ());
    assert!(result.is_err());

    // PROTECTION: Cannot mutate via DataMapMut because MutableLimitNode
    // doesn't implement ImmutableEdgeBounds
    // The following line would fail to compile if uncommented:
    // if let Some(node) = DataMapMut::node_weight_mut(&mut graph, n1) {
    //     node.max_edges = 100;  // This would break our guarantees!
    // }

    // However, we CAN still access data through direct indexing (read-only safe)
    // and we can demonstrate the data field is being used:
    let data_copy = graph[n1].data.clone();
    assert_eq!(data_copy, "Node1");

    // If we could mutate max_edges (which we can't safely via DataMapMut),
    // it would break the edge limiting guarantees. The type system prevents this!

    // Verify we still can't add edges - limits are enforced
    let result = graph.add_edge(n1, n2, ());
    assert!(result.is_err());
}

// A safe implementation: edge bounds are determined by const generics,
// even though we store mutable data
#[derive(Debug, Clone)]
struct SafeNodeWithData<const MAX_IN: usize, const MAX_OUT: usize> {
    // This data can be mutated safely
    label: String,
    value: i32,
}

impl<const MAX_IN: usize, const MAX_OUT: usize> EdgeBounds for SafeNodeWithData<MAX_IN, MAX_OUT> {
    fn max_incoming_edges(&self) -> DefaultIx {
        MAX_IN as DefaultIx
    }

    fn max_outgoing_edges(&self) -> DefaultIx {
        MAX_OUT as DefaultIx
    }
}

impl<const MAX_IN: usize, const MAX_OUT: usize> SimpleEdgeBounds
    for SafeNodeWithData<MAX_IN, MAX_OUT>
{
}

// SAFE: Edge bounds are const generics, cannot be changed
impl<const MAX_IN: usize, const MAX_OUT: usize> ImmutableEdgeBounds
    for SafeNodeWithData<MAX_IN, MAX_OUT>
{
}

#[test]
fn test_safe_mutation_with_immutable_edge_bounds() {
    let mut graph = BoundedGraph::<SafeNodeWithData<2, 3>, ()>::new();

    let n1 = graph.add_node(SafeNodeWithData {
        label: "Node A".to_string(),
        value: 100,
    });

    let n2 = graph.add_node(SafeNodeWithData {
        label: "Node B".to_string(),
        value: 200,
    });

    assert_eq!(graph[n1].label, "Node A");
    assert_eq!(graph[n1].value, 100);
    assert_eq!(graph[n2].label, "Node B");
    assert_eq!(graph[n2].value, 200);

    // Add edges - n2 can accept 2 incoming
    assert!(graph.add_edge(n1, n2, ()).is_ok());
    assert!(graph.add_edge(n1, n2, ()).is_ok());

    // Mutate the data - this is SAFE because edge limits are const generics
    if let Some(node) = DataMapMut::node_weight_mut(&mut graph, n1) {
        node.label = "Node A (modified)".to_string();
        node.value = 999;
    }

    assert_eq!(graph[n1].label, "Node A (modified)");
    assert_eq!(graph[n1].value, 999);

    // Edge limits remain enforced
    // n1 can have up to 3 outgoing but n2 can only accept 2 incoming, so adding more to n2 fails
    let result = graph.add_edge(n1, n2, ());
    assert!(result.is_err());

    // Can add to a different target (n1 still has 1 more outgoing slot)
    let n3 = graph.add_node(SafeNodeWithData {
        label: "Node C".to_string(),
        value: 300,
    });

    // n1 has 2 outgoing edges so far, can add 1 more (max is 3)
    assert!(graph.add_edge(n1, n3, ()).is_ok());

    // Now n1 is at capacity for outgoing edges
    let result = graph.add_edge(n1, n3, ());
    assert!(result.is_err());
}

#[test]
fn test_data_mutation_preserves_edge_limits() {
    // Using SymmetricFixedEdgeCount with mutable data
    #[derive(Debug, Clone, PartialEq)]
    struct NodeData {
        count: i32,
        name: String,
    }

    let mut graph = BoundedGraph::<SymmetricFixedEdgeCount<2, NodeData>, ()>::new();
    let n1 = graph.add_node(SymmetricFixedEdgeCount::new(NodeData {
        count: 0,
        name: "test".to_string(),
    }));
    let n2 = graph.add_node(SymmetricFixedEdgeCount::new(NodeData {
        count: 10,
        name: "node2".to_string(),
    }));

    // Add edges to capacity
    assert!(graph.add_edge(n1, n2, ()).is_ok());
    assert!(graph.add_edge(n1, n2, ()).is_ok());

    // Mutate data via DataMapMut
    if let Some(node) = DataMapMut::node_weight_mut(&mut graph, n1) {
        node.count += 100;
        node.name = "mutated".to_string();
    }

    assert_eq!(graph[n1].count, 100);
    assert_eq!(graph[n1].name, "mutated");

    // Edge limits still enforced
    let result = graph.add_edge(n1, n2, ());
    assert!(result.is_err());
}

#[test]
fn test_deref_mut_with_immutable_edge_bounds() {
    use std::ops::DerefMut;

    // With ImmutableEdgeBounds, DerefMut is available
    let mut graph = BoundedGraph::<SymmetricFixedEdgeCount<2, String>, ()>::new();
    let n1 = graph.add_node(SymmetricFixedEdgeCount::new("Node1".to_string()));
    let n2 = graph.add_node(SymmetricFixedEdgeCount::new("Node2".to_string()));

    // Add edges to capacity
    assert!(graph.add_edge(n1, n2, ()).is_ok());
    assert!(graph.add_edge(n1, n2, ()).is_ok());
    assert!(graph.add_edge(n1, n2, ()).is_err()); // At capacity

    // DerefMut allows us to get mutable reference to underlying graph
    // This gives access to the underlying petgraph methods
    let underlying_graph = graph.deref_mut();

    // We can use the underlying graph methods
    // For example, check the edge count directly
    assert_eq!(underlying_graph.edge_count(), 2);

    // We can also access node weights via the underlying graph's Index trait
    if let Some(node_weight) = underlying_graph.node_weight_mut(n1) {
        // Mutate the data field (safe because MAX is const)
        node_weight.data = "Modified".to_string();
    }

    assert_eq!(graph[n1].data, "Modified");

    // Edge limits are still enforced through BoundedGraph methods
    assert!(graph.add_edge(n1, n2, ()).is_err());
}

#[test]
fn test_deref_mut_unavailable_for_mutable_bounds() {
    // This test demonstrates that DerefMut is NOT available for nodes
    // with mutable edge bounds, protecting the edge limiting guarantees

    let mut graph = BoundedGraph::<MutableLimitNode, ()>::new();
    let n1 = graph.add_node(MutableLimitNode {
        max_edges: 2,
        data: "Node1".to_string(),
    });

    // Deref is always available (immutable access)
    let _immutable_ref = &*graph;
    assert_eq!(_immutable_ref.node_count(), 1);

    // However, DerefMut is NOT available because MutableLimitNode
    // doesn't implement ImmutableEdgeBounds.
    // The following would fail to compile if uncommented:
    //
    // use std::ops::DerefMut;
    // let underlying_graph = graph.deref_mut();
    //
    // This is intentional! It prevents code from bypassing the safety checks
    // and mutating edge bounds through the underlying graph.

    // We can still use read-only access
    assert_eq!(graph[n1].data, "Node1");
}