openquiver-graph 0.1.0

OpenQuiver graph IR: nodes, pins, types. Bottom layer, no internal dependencies.
Documentation
//! Editing an existing graph.
//!
//! Renaming is the deal this IR consciously accepted in exchange for letting a
//! node's name be its identity: it is a refactor, not a field assignment. These
//! tests pin down that the refactor is complete — every wire follows, and so
//! does every comment — because a rename that quietly drops either is worse
//! than not offering one.

use openquiver_graph::{
    Anchor, Document, Edge, EdgeKind, Graph, Layout, Node, NodeName, NodeTypeId, PinRef, Trivia,
};

fn pin(node: &str, pin: &str) -> PinRef {
    PinRef::new(NodeName::new(node), pin)
}

fn wired() -> Document {
    let mut graph = Graph::new("g");
    for name in ["ask", "out", "start"] {
        graph.insert_node(Node::new(NodeName::new(name), NodeTypeId::new("t.x")));
    }
    graph.insert_edge(Edge::new(
        EdgeKind::Exec,
        pin("start", "out"),
        pin("ask", "in"),
    ));
    graph.insert_edge(Edge::new(
        EdgeKind::Data,
        pin("ask", "text"),
        pin("out", "value"),
    ));

    let mut trivia = Trivia::default();
    trivia.add(
        Anchor::Node(NodeName::new("ask")),
        vec!["// the agent".to_owned()],
    );
    trivia.add(
        Anchor::Edge(Edge::new(
            EdgeKind::Data,
            pin("ask", "text"),
            pin("out", "value"),
        )),
        vec!["// the answer".to_owned()],
    );

    let mut layout = Layout::default();
    layout.set(NodeName::new("ask"), (80, 40));

    Document::new(graph, trivia, layout)
}

#[test]
fn renaming_rewrites_every_wire_that_mentions_the_node() {
    let mut doc = wired();

    doc.rename_node("ask", "prompt")
        .expect("rename should succeed");

    assert!(doc.graph.node("ask").is_none(), "the old name is gone");
    assert!(
        doc.graph.node("prompt").is_some(),
        "the new name is present"
    );

    let mentions: Vec<_> = doc
        .graph
        .edges()
        .filter(|e| e.from().node() == "ask" || e.to().node() == "ask")
        .collect();
    assert!(
        mentions.is_empty(),
        "wires still point at the old name: {mentions:?}"
    );
    assert_eq!(
        doc.graph.edges().count(),
        2,
        "no wire was lost along the way"
    );
}

#[test]
fn renaming_carries_the_comments_across() {
    let mut doc = wired();

    doc.rename_node("ask", "prompt")
        .expect("rename should succeed");

    // Comments are keyed by identity, so a rename that does not move them
    // deletes them — silently, on the next save.
    assert_eq!(
        doc.trivia.get(&Anchor::Node(NodeName::new("prompt"))),
        ["// the agent".to_owned()],
        "the node's own comment did not follow it"
    );

    let moved = Edge::new(EdgeKind::Data, pin("prompt", "text"), pin("out", "value"));
    assert_eq!(
        doc.trivia.get(&Anchor::Edge(moved)),
        ["// the answer".to_owned()],
        "the comment on a wire touching the node did not follow it"
    );
}

#[test]
fn renaming_onto_a_name_that_is_taken_is_refused() {
    let mut doc = wired();

    // Silently merging two nodes into one identity is the duplicate-name
    // failure by another route.
    doc.rename_node("ask", "out")
        .expect_err("should be refused");

    assert!(
        doc.graph.node("ask").is_some(),
        "the graph was left untouched"
    );
    assert_eq!(doc.graph.edges().count(), 2);
}

#[test]
fn removing_a_node_takes_its_wires_with_it() {
    let mut doc = wired();

    doc.remove_node("ask");

    // A dangling wire is not a state the compiler should ever have to consider,
    // and leaving one lets a later node reuse the name and inherit the wires.
    assert_eq!(doc.graph.edges().count(), 0);
    assert!(
        doc.trivia
            .get(&Anchor::Node(NodeName::new("ask")))
            .is_empty()
    );
}

#[test]
fn wires_can_be_found_by_the_pin_they_touch() {
    let doc = wired();

    // The editor needs this to implement Blueprint's rule that connecting a
    // second wire to an exec output replaces the first, and the compiler needs
    // it to check pin cardinality at all.
    assert_eq!(doc.graph.edges_from(&pin("start", "out")).count(), 1);
    assert_eq!(doc.graph.edges_into(&pin("ask", "in")).count(), 1);
    assert_eq!(doc.graph.edges_into(&pin("start", "out")).count(), 0);
}

#[test]
fn removing_by_a_denormalized_name_still_takes_the_wires() {
    let mut doc = Document::new(Graph::new("g"), Trivia::default(), Layout::default());
    doc.graph
        .insert_node(Node::new(NodeName::new("é"), NodeTypeId::new("t.x")));
    doc.graph
        .insert_node(Node::new(NodeName::new("b"), NodeTypeId::new("t.y")));
    doc.graph
        .insert_edge(Edge::new(EdgeKind::Exec, pin("é", "out"), pin("b", "in")));

    // "e" plus a combining acute: what macOS filesystems and several input
    // methods produce. The node used to be found by its normalized key while
    // the wires were matched against the caller's raw string, so the node went
    // and the wires stayed — exactly the dangling state this must never leave.
    doc.remove_node("e\u{0301}");

    assert!(doc.graph.node("é").is_none(), "the node is gone");
    assert_eq!(doc.graph.edges().count(), 0, "and so are its wires");
}