jellyflow-core 0.2.0

Headless graph model, IDs, type descriptors, and interaction policy primitives for Jellyflow.
Documentation
use super::*;

#[test]
fn remove_group_detaches_child_nodes_and_inverts() {
    let mut graph = Graph::default();
    let group_id = GroupId::new();
    graph.groups.insert(
        group_id,
        Group {
            title: "Group".to_string(),
            rect: crate::core::CanvasRect {
                origin: CanvasPoint { x: 0.0, y: 0.0 },
                size: crate::core::CanvasSize {
                    width: 100.0,
                    height: 100.0,
                },
            },
            color: None,
        },
    );

    let node_id = NodeId::new();
    let mut node = make_node("core.a");
    node.parent = Some(group_id);
    graph.nodes.insert(node_id, node);

    let tx = graph
        .build_remove_group_tx(group_id, "Delete Group")
        .expect("tx");
    apply_transaction(&mut graph, &tx).expect("apply");
    assert!(!graph.groups.contains_key(&group_id));
    assert_eq!(graph.nodes.get(&node_id).unwrap().parent, None);

    let undo = invert_transaction(&tx);
    apply_transaction(&mut graph, &undo).expect("undo apply");
    assert!(graph.groups.contains_key(&group_id));
    assert_eq!(graph.nodes.get(&node_id).unwrap().parent, Some(group_id));
}

#[test]
fn set_group_rect_and_child_positions_roundtrip_through_invert_transaction() {
    let mut graph = Graph::default();
    let group_id = GroupId::new();
    let rect0 = crate::core::CanvasRect {
        origin: CanvasPoint { x: 10.0, y: 20.0 },
        size: crate::core::CanvasSize {
            width: 100.0,
            height: 80.0,
        },
    };
    graph.groups.insert(
        group_id,
        Group {
            title: "Group".to_string(),
            rect: rect0,
            color: None,
        },
    );

    let node_a = NodeId::new();
    let node_b = NodeId::new();
    let mut a = make_node("core.a");
    a.parent = Some(group_id);
    a.pos = CanvasPoint { x: 30.0, y: 40.0 };
    let mut b = make_node("core.b");
    b.parent = Some(group_id);
    b.pos = CanvasPoint { x: 50.0, y: 60.0 };
    graph.nodes.insert(node_a, a);
    graph.nodes.insert(node_b, b);

    let rect1 = crate::core::CanvasRect {
        origin: CanvasPoint { x: 110.0, y: 120.0 },
        size: rect0.size,
    };

    let tx = GraphTransaction::from_ops([
        GraphOp::SetGroupRect {
            id: group_id,
            from: rect0,
            to: rect1,
        },
        GraphOp::SetNodePos {
            id: node_a,
            from: CanvasPoint { x: 30.0, y: 40.0 },
            to: CanvasPoint { x: 130.0, y: 140.0 },
        },
        GraphOp::SetNodePos {
            id: node_b,
            from: CanvasPoint { x: 50.0, y: 60.0 },
            to: CanvasPoint { x: 150.0, y: 160.0 },
        },
    ])
    .with_label("Move Group");

    apply_transaction(&mut graph, &tx).expect("apply");
    assert_eq!(graph.groups.get(&group_id).unwrap().rect, rect1);
    assert_eq!(
        graph.nodes.get(&node_a).unwrap().pos,
        CanvasPoint { x: 130.0, y: 140.0 }
    );
    assert_eq!(
        graph.nodes.get(&node_b).unwrap().pos,
        CanvasPoint { x: 150.0, y: 160.0 }
    );

    let undo = invert_transaction(&tx);
    apply_transaction(&mut graph, &undo).expect("undo apply");
    assert_eq!(graph.groups.get(&group_id).unwrap().rect, rect0);
    assert_eq!(
        graph.nodes.get(&node_a).unwrap().pos,
        CanvasPoint { x: 30.0, y: 40.0 }
    );
    assert_eq!(
        graph.nodes.get(&node_b).unwrap().pos,
        CanvasPoint { x: 50.0, y: 60.0 }
    );
}

#[test]
fn set_node_size_roundtrips_through_invert_transaction() {
    let mut graph = Graph::default();
    let node_id = NodeId::new();
    insert_node(&mut graph, node_id, "core.a");

    let tx = GraphTransaction::from_ops([GraphOp::SetNodeSize {
        id: node_id,
        from: None,
        to: Some(crate::core::CanvasSize {
            width: 333.0,
            height: 222.0,
        }),
    }])
    .with_label("Resize");

    apply_transaction(&mut graph, &tx).expect("apply");
    assert_eq!(
        graph.nodes.get(&node_id).unwrap().size,
        Some(crate::core::CanvasSize {
            width: 333.0,
            height: 222.0,
        })
    );

    let undo = invert_transaction(&tx);
    apply_transaction(&mut graph, &undo).expect("undo apply");
    assert_eq!(graph.nodes.get(&node_id).unwrap().size, None);
}

#[test]
fn set_group_title_roundtrips_through_invert_transaction() {
    let mut graph = Graph::default();
    let group_id = GroupId::new();
    graph.groups.insert(
        group_id,
        Group {
            title: "Group".to_string(),
            rect: crate::core::CanvasRect {
                origin: CanvasPoint { x: 0.0, y: 0.0 },
                size: crate::core::CanvasSize {
                    width: 100.0,
                    height: 100.0,
                },
            },
            color: None,
        },
    );

    let tx = GraphTransaction::from_ops([GraphOp::SetGroupTitle {
        id: group_id,
        from: "Group".to_string(),
        to: "My Group".to_string(),
    }])
    .with_label("Rename Group");

    apply_transaction(&mut graph, &tx).expect("apply");
    assert_eq!(graph.groups.get(&group_id).unwrap().title, "My Group");

    let undo = invert_transaction(&tx);
    apply_transaction(&mut graph, &undo).expect("undo apply");
    assert_eq!(graph.groups.get(&group_id).unwrap().title, "Group");
}

#[test]
fn set_node_data_roundtrips_through_invert_transaction() {
    let mut graph = Graph::default();
    let node = NodeId::new();
    insert_node(&mut graph, node, "demo.const");

    let tx = GraphTransaction::from_ops([GraphOp::SetNodeData {
        id: node,
        from: serde_json::Value::Null,
        to: serde_json::json!({ "value": 1.25 }),
    }])
    .with_label("Set value");

    apply_transaction(&mut graph, &tx).expect("apply");
    assert_eq!(
        graph.nodes.get(&node).unwrap().data,
        serde_json::json!({ "value": 1.25 })
    );

    let inverse = invert_transaction(&tx);
    apply_transaction(&mut graph, &inverse).expect("apply inverse");
    assert_eq!(
        graph.nodes.get(&node).unwrap().data,
        serde_json::Value::Null
    );
}

#[test]
fn set_edge_endpoints_updates_edge_in_place() {
    let mut graph = Graph::default();
    let a = NodeId::new();
    let b = NodeId::new();
    let c = NodeId::new();
    insert_node(&mut graph, a, "core.a");
    insert_node(&mut graph, b, "core.b");
    insert_node(&mut graph, c, "core.c");

    let out1 = PortId::new();
    let out2 = PortId::new();
    let inn = PortId::new();
    insert_port(&mut graph, out1, a, "out1", PortDirection::Out);
    insert_port(&mut graph, out2, c, "out2", PortDirection::Out);
    insert_port(&mut graph, inn, b, "in", PortDirection::In);

    let edge_id = EdgeId::new();
    graph.edges.insert(edge_id, make_edge(out1, inn));

    let tx = GraphTransaction::from_ops([GraphOp::SetEdgeEndpoints {
        id: edge_id,
        from: EdgeEndpoints {
            from: out1,
            to: inn,
        },
        to: EdgeEndpoints {
            from: out2,
            to: inn,
        },
    }]);
    apply_transaction(&mut graph, &tx).expect("apply");

    let edge = graph.edges.get(&edge_id).expect("edge");
    assert_eq!(edge.from, out2);
    assert_eq!(edge.to, inn);
}

#[test]
fn symbol_setters_roundtrip_through_normalize_and_invert() {
    let mut graph = Graph::default();
    let symbol_id = SymbolId::new();
    graph.symbols.insert(
        symbol_id,
        Symbol {
            name: "A".to_string(),
            ty: None,
            default_value: None,
            meta: serde_json::Value::Null,
        },
    );

    let baseline = serde_json::to_value(&graph).unwrap();

    let mut tx = GraphTransaction::new();
    tx.push(GraphOp::SetSymbolName {
        id: symbol_id,
        from: "A".to_string(),
        to: "B".to_string(),
    });
    tx.push(GraphOp::SetSymbolName {
        id: symbol_id,
        from: "B".to_string(),
        to: "C".to_string(),
    });
    tx.push(GraphOp::SetSymbolType {
        id: symbol_id,
        from: None,
        to: Some(TypeDesc::Int),
    });
    tx.push(GraphOp::SetSymbolDefaultValue {
        id: symbol_id,
        from: None,
        to: Some(serde_json::json!(123)),
    });

    let tx = crate::ops::normalize_transaction(tx);
    assert!(tx.len() < 4, "expected normalize to coalesce setter chain");

    apply_transaction(&mut graph, &tx).expect("apply forward");
    assert_eq!(graph.symbols.get(&symbol_id).unwrap().name, "C");
    assert_eq!(
        graph.symbols.get(&symbol_id).unwrap().ty,
        Some(TypeDesc::Int)
    );
    assert_eq!(
        graph.symbols.get(&symbol_id).unwrap().default_value,
        Some(serde_json::json!(123))
    );

    let inverse = invert_transaction(&tx);
    apply_transaction(&mut graph, &inverse).expect("apply inverse");
    assert_eq!(serde_json::to_value(&graph).unwrap(), baseline);
}

#[test]
fn graph_import_ops_roundtrip_through_normalize_and_invert() {
    let mut graph = Graph::default();
    let baseline = serde_json::to_value(&graph).unwrap();

    let imported = GraphId::from_u128(2);
    let import = GraphImport {
        alias: Some("math".to_string()),
    };

    let mut tx = GraphTransaction::new();
    tx.push(GraphOp::AddImport {
        id: imported,
        import: import.clone(),
    });
    tx.push(GraphOp::SetImportAlias {
        id: imported,
        from: Some("math".to_string()),
        to: Some("stdlib".to_string()),
    });
    tx.push(GraphOp::SetImportAlias {
        id: imported,
        from: Some("stdlib".to_string()),
        to: None,
    });

    let tx = crate::ops::normalize_transaction(tx);
    apply_transaction(&mut graph, &tx).expect("apply");
    assert!(graph.imports.contains_key(&imported));
    assert_eq!(graph.imports.get(&imported).unwrap().alias, None);

    let inverse = invert_transaction(&tx);
    apply_transaction(&mut graph, &inverse).expect("apply inverse");
    assert_eq!(serde_json::to_value(&graph).unwrap(), baseline);
}