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 petgraph::{
    csr::IndexType,
    dot::{Config, Dot},
    Direction::Incoming,
};

use crate::{BoundedGraph, EdgeBounds, SimpleEdgeBounds};

#[test]
fn edge_number_bounded_node() {
    #[derive(Debug, Default)]
    pub struct BoundedNodeIndex {
        name: String,
        inputs: usize,
        outputs: usize,
    }

    impl BoundedNodeIndex {
        pub fn new(name: String, inputs: usize, outputs: usize) -> Self {
            Self {
                name,
                inputs,
                outputs,
            }
        }
    }

    impl<Ix: IndexType> EdgeBounds<Ix> for BoundedNodeIndex {
        fn max_incoming_edges(&self) -> Ix {
            Ix::new(self.inputs)
        }

        fn max_outgoing_edges(&self) -> Ix {
            Ix::new(self.outputs)
        }
    }

    impl SimpleEdgeBounds for BoundedNodeIndex {}

    let mut deps = BoundedGraph::<BoundedNodeIndex, &str>::new();
    let pg = deps.add_node(BoundedNodeIndex::new(
        "petgraph".to_string(),
        0usize,
        3usize,
    ));
    let fb = deps.add_node(BoundedNodeIndex::new(
        "fixedbitset".to_string(),
        4usize,
        0usize,
    ));
    let qc = deps.add_node(BoundedNodeIndex::new(
        "quickcheck".to_string(),
        1usize,
        2usize,
    ));
    let rand = deps.add_node(BoundedNodeIndex::new("rand".to_string(), 1usize, 2usize));
    let libc = deps.add_node(BoundedNodeIndex::new("libc".to_string(), 1usize, 2usize));
    deps.extend_with_edges(&[(pg, fb), (pg, qc), (qc, rand), (rand, libc), (qc, libc)]);

    println!("{:?}", Dot::with_config(&*deps, &[Config::EdgeNoLabel]));

    assert_eq!(deps[pg].name, "petgraph");
    assert_eq!(deps.edge_count(), 4); // One should be missing.
    assert_eq!(deps.edges_directed(libc, Incoming).count(), 1); // It should be missing from here.

    deps.update_edge(qc, fb, "").unwrap();
    assert_eq!(deps.edge_count(), 5);
}

// Test type for advanced_node test - uses FixedEdgeCount for simplicity
use crate::FixedEdgeCount;

#[test]
fn advanced_node() {
    // Simplified version using FixedEdgeCount instead of custom node type
    let mut deps = BoundedGraph::<FixedEdgeCount<3>, &str>::new();
    let pg = deps.add_node(FixedEdgeCount::default());
    let fb = deps.add_node(FixedEdgeCount::default());
    let qc = deps.add_node(FixedEdgeCount::default());
    deps.extend_with_edges(&[(pg, fb), (pg, qc)]);
    println!("{:?}", Dot::with_config(&*deps, &[Config::EdgeNoLabel]));
    assert_eq!(deps.node_count(), 3);
    assert_eq!(deps.edge_count(), 2);
}

#[test]
fn test_edge_bounds_at_limit() {
    use crate::BoundedNode;
    use petgraph::graph::DefaultIx;
    use petgraph::Direction;

    #[derive(Debug)]
    pub struct SimpleBoundedNode {
        max_in: usize,
        max_out: usize,
    }

    impl<Ix: IndexType> EdgeBounds<Ix> for SimpleBoundedNode {
        fn max_incoming_edges(&self) -> Ix {
            Ix::new(self.max_in)
        }

        fn max_outgoing_edges(&self) -> Ix {
            Ix::new(self.max_out)
        }
    }

    impl SimpleEdgeBounds for SimpleBoundedNode {}

    let node = SimpleBoundedNode {
        max_in: 3,
        max_out: 2,
    };

    // Test at limit - should return false
    assert!(
        !<SimpleBoundedNode as BoundedNode<DefaultIx>>::can_add_edge(
            &node,
            Direction::Incoming,
            3,
            &node
        )
    );
    assert!(
        !<SimpleBoundedNode as BoundedNode<DefaultIx>>::can_add_edge(
            &node,
            Direction::Outgoing,
            2,
            &node
        )
    );

    // Test one below limit - should return true
    assert!(<SimpleBoundedNode as BoundedNode<DefaultIx>>::can_add_edge(
        &node,
        Direction::Incoming,
        2,
        &node
    ));
    assert!(<SimpleBoundedNode as BoundedNode<DefaultIx>>::can_add_edge(
        &node,
        Direction::Outgoing,
        1,
        &node
    ));

    // Test at zero - should return true
    assert!(<SimpleBoundedNode as BoundedNode<DefaultIx>>::can_add_edge(
        &node,
        Direction::Incoming,
        0,
        &node
    ));
    assert!(<SimpleBoundedNode as BoundedNode<DefaultIx>>::can_add_edge(
        &node,
        Direction::Outgoing,
        0,
        &node
    ));
}

#[test]
fn test_relational_constraints_with_other_node() {
    use crate::{BoundedGraph, BoundedNode, ImmutableEdgeBounds};
    use petgraph::Direction;

    /// A node that can only connect to nodes with compatible roles.
    /// Demonstrates using the `other_node` parameter for relational constraints.
    #[derive(Debug, Clone, PartialEq)]
    enum NodeRole {
        Server,
        Client,
        Peer,
    }

    #[derive(Debug, Clone)]
    struct RoleBasedNode {
        role: NodeRole,
        max_connections: usize,
    }

    impl RoleBasedNode {
        fn new(role: NodeRole, max_connections: usize) -> Self {
            Self {
                role,
                max_connections,
            }
        }

        fn can_connect_to(&self, other: &Self) -> bool {
            match (&self.role, &other.role) {
                // Servers can connect to clients and peers
                (NodeRole::Server, NodeRole::Client) => true,
                (NodeRole::Server, NodeRole::Peer) => true,
                // Clients can only connect to servers
                (NodeRole::Client, NodeRole::Server) => true,
                // Peers can connect to servers and other peers
                (NodeRole::Peer, NodeRole::Server) => true,
                (NodeRole::Peer, NodeRole::Peer) => true,
                // All other combinations are rejected
                _ => false,
            }
        }
    }

    impl BoundedNode for RoleBasedNode {
        fn can_add_edge(
            &self,
            dir: Direction,
            existing_edge_count: usize,
            other_node: &Self,
        ) -> bool {
            // First check capacity
            if existing_edge_count >= self.max_connections {
                return false;
            }

            // Then check role compatibility based on direction
            match dir {
                Direction::Outgoing => self.can_connect_to(other_node),
                Direction::Incoming => other_node.can_connect_to(self),
            }
        }
    }

    // Allow mutable access since constraints are based on role (immutable)
    impl ImmutableEdgeBounds for RoleBasedNode {}

    let mut graph = BoundedGraph::<RoleBasedNode, ()>::new();

    let server1 = graph.add_node(RoleBasedNode::new(NodeRole::Server, 10));
    let server2 = graph.add_node(RoleBasedNode::new(NodeRole::Server, 10));
    let client1 = graph.add_node(RoleBasedNode::new(NodeRole::Client, 3));
    let client2 = graph.add_node(RoleBasedNode::new(NodeRole::Client, 3));
    let peer1 = graph.add_node(RoleBasedNode::new(NodeRole::Peer, 5));
    let peer2 = graph.add_node(RoleBasedNode::new(NodeRole::Peer, 5));

    // Valid connections: Server -> Client
    assert!(graph.add_edge(server1, client1, ()).is_ok()); // Edge 1
    assert!(graph.add_edge(server1, client2, ()).is_ok()); // Edge 2

    // Valid connections: Client -> Server
    assert!(graph.add_edge(client1, server2, ()).is_ok()); // Edge 3

    // Invalid: Client cannot connect to another Client
    let result = graph.add_edge(client1, client2, ());
    assert!(
        result.is_err(),
        "Client->Client should be rejected by role rules"
    );

    // Valid: Server -> Peer
    assert!(graph.add_edge(server1, peer1, ()).is_ok()); // Edge 4

    // Valid: Peer -> Peer
    assert!(graph.add_edge(peer1, peer2, ()).is_ok()); // Edge 5

    // Valid: Peer -> Server
    assert!(graph.add_edge(peer1, server2, ()).is_ok()); // Edge 6

    // Invalid: Server cannot connect to another Server
    let result = graph.add_edge(server1, server2, ());
    assert!(
        result.is_err(),
        "Server->Server should be rejected by role rules"
    );

    // Test capacity limits still work
    let client3 = graph.add_node(RoleBasedNode::new(NodeRole::Client, 1));
    assert!(graph.add_edge(client3, server1, ()).is_ok()); // Edge 7: First connection OK

    let result = graph.add_edge(client3, server2, ());
    assert!(
        result.is_err(),
        "Should exceed capacity (max=1 outgoing for client3)"
    );

    // Verify final edge count - should have 7 successful edges
    assert_eq!(graph.edge_count(), 7);

    println!("Successfully demonstrated relational constraints using other_node parameter!");
}