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, BoundedGraphError, FixedEdgeCount, SymmetricFixedEdgeCount};

#[test]
fn test_asymmetric_edge_limits() {
    // Create nodes with 2 max incoming, 5 max outgoing edges
    let mut graph = BoundedGraph::<FixedEdgeCount<2, 5>, ()>::new();

    let n1 = graph.add_node(FixedEdgeCount::empty());
    let n2 = graph.add_node(FixedEdgeCount::empty());
    let n3 = graph.add_node(FixedEdgeCount::empty());
    let n4 = graph.add_node(FixedEdgeCount::empty());
    let n5 = graph.add_node(FixedEdgeCount::empty());
    let n6 = graph.add_node(FixedEdgeCount::empty());

    // n1 can have up to 5 outgoing edges
    assert!(graph.add_edge(n1, n2, ()).is_ok());
    assert!(graph.add_edge(n1, n3, ()).is_ok());
    assert!(graph.add_edge(n1, n4, ()).is_ok());
    assert!(graph.add_edge(n1, n5, ()).is_ok());
    assert!(graph.add_edge(n1, n6, ()).is_ok());

    // 6th outgoing edge should fail
    let n7 = graph.add_node(FixedEdgeCount::empty());
    let result = graph.add_edge(n1, n7, ());
    assert!(result.is_err());
    assert!(matches!(
        result.unwrap_err(),
        BoundedGraphError::EdgeRejected {
            source_rejected: true,
            target_rejected: false,
            ..
        }
    ));

    // n2 can only have 2 incoming edges
    assert!(graph.add_edge(n3, n2, ()).is_ok()); // First incoming to n2 (already has one from n1)

    // 3rd incoming edge to n2 should fail
    let result = graph.add_edge(n4, n2, ());
    assert!(result.is_err());
    assert!(matches!(
        result.unwrap_err(),
        BoundedGraphError::EdgeRejected {
            source_rejected: false,
            target_rejected: true,
            ..
        }
    ));
}

#[test]
fn test_asymmetric_hub_and_spoke() {
    // Hub node: many outgoing, few incoming (like a server)
    // Spoke nodes: few outgoing, many incoming (like clients)
    type Hub = FixedEdgeCount<1, 10>; // 1 in, 10 out
    type Spoke = FixedEdgeCount<10, 1>; // 10 in, 1 out

    // Create a mixed graph with both hub and spoke types
    let mut hub_graph = BoundedGraph::<Hub, ()>::new();
    let hub = hub_graph.add_node(FixedEdgeCount::empty());

    // Add hub-type nodes as spokes
    let mut spokes = Vec::new();
    for _ in 0..10 {
        spokes.push(hub_graph.add_node(FixedEdgeCount::empty()));
    }

    // Connect hub to all spokes
    for &spoke in &spokes {
        assert!(hub_graph.add_edge(hub, spoke, ()).is_ok());
    }

    // Hub is now at capacity for outgoing edges (10 out of 10)
    let extra_spoke = hub_graph.add_node(FixedEdgeCount::empty());
    let result = hub_graph.add_edge(hub, extra_spoke, ());
    assert!(result.is_err());
    assert_eq!(hub_graph.edge_count(), 10);

    // Now demonstrate with actual Spoke-type nodes
    let mut spoke_graph = BoundedGraph::<Spoke, ()>::new();
    let spoke1 = spoke_graph.add_node(FixedEdgeCount::empty());
    let spoke2 = spoke_graph.add_node(FixedEdgeCount::empty());
    let spoke3 = spoke_graph.add_node(FixedEdgeCount::empty());

    // Spoke can only send 1 outgoing edge
    assert!(spoke_graph.add_edge(spoke1, spoke2, ()).is_ok());
    let result = spoke_graph.add_edge(spoke1, spoke3, ());
    assert!(
        result.is_err(),
        "Spoke should be limited to 1 outgoing edge"
    );

    // But spoke can receive many incoming edges (up to 10)
    assert!(spoke_graph.add_edge(spoke2, spoke3, ()).is_ok());
    // spoke3 now has 2 incoming edges, can accept 8 more
    assert_eq!(spoke_graph.edge_count(), 2);
}

#[test]
fn test_symmetric_vs_asymmetric() {
    // Compare symmetric and asymmetric limits
    let mut symmetric = BoundedGraph::<SymmetricFixedEdgeCount<3>, ()>::new();
    let n1_sym = symmetric.add_node(SymmetricFixedEdgeCount::empty());
    let n2_sym = symmetric.add_node(SymmetricFixedEdgeCount::empty());

    // Can add 3 in each direction
    for _ in 0..3 {
        assert!(symmetric.add_edge(n1_sym, n2_sym, ()).is_ok());
    }
    for _ in 0..3 {
        assert!(symmetric.add_edge(n2_sym, n1_sym, ()).is_ok());
    }

    assert_eq!(symmetric.edge_count(), 6);

    // With asymmetric: 2 in, 4 out
    let mut asymmetric = BoundedGraph::<FixedEdgeCount<2, 4>, ()>::new();
    let n1_asym = asymmetric.add_node(FixedEdgeCount::empty());
    let n2_asym = asymmetric.add_node(FixedEdgeCount::empty());

    // Try to add 4 edges from n1 to n2, but n2 can only accept 2 incoming
    assert!(asymmetric.add_edge(n1_asym, n2_asym, ()).is_ok());
    assert!(asymmetric.add_edge(n1_asym, n2_asym, ()).is_ok());
    // 3rd and 4th fail because n2 is at incoming capacity
    assert!(asymmetric.add_edge(n1_asym, n2_asym, ()).is_err());
    assert!(asymmetric.add_edge(n1_asym, n2_asym, ()).is_err());
    assert_eq!(asymmetric.edge_count(), 2);

    // n2 can have 4 outgoing, and n1 can accept 2 incoming
    assert!(asymmetric.add_edge(n2_asym, n1_asym, ()).is_ok());
    assert!(asymmetric.add_edge(n2_asym, n1_asym, ()).is_ok());
    // n1 is now at incoming capacity
    assert!(asymmetric.add_edge(n2_asym, n1_asym, ()).is_err());

    assert_eq!(asymmetric.edge_count(), 4);
}