use petgraph_decypher::{
CypherEdge, CypherNode, CypherProperties, CypherValue, NodeData, build_graph_from_cypher,
build_graph_from_cypher_typed,
};
use std::collections::HashMap;
#[test]
fn build_graph_single_node() {
let g = build_graph_from_cypher("CREATE (n:Person {name: \"Alice\"})").unwrap();
assert_eq!(g.node_count(), 1);
assert_eq!(g.edge_count(), 0);
let n = g.node_indices().next().unwrap();
let data: &NodeData = &g[n];
assert_eq!(data.labels, vec!["Person"]);
assert_eq!(
data.properties.get("name"),
Some(&CypherValue::String("Alice".into()))
);
}
#[test]
fn build_graph_two_nodes_one_edge() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})"#,
)
.unwrap();
assert_eq!(g.node_count(), 2);
assert_eq!(g.edge_count(), 1);
let e = g.edge_indices().next().unwrap();
assert_eq!(g[e].rel_type.as_deref(), Some("KNOWS"));
}
#[test]
fn build_graph_shared_variable_reuses_node() {
let g = build_graph_from_cypher(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS]->(b:Person {name: "Bob"})
CREATE (a)-[:LIKES]->(c:Thing {name: "Coffee"})"#,
)
.unwrap();
assert_eq!(g.node_count(), 3);
assert_eq!(g.edge_count(), 2);
}
#[test]
fn build_graph_chain_of_nodes() {
let g = build_graph_from_cypher("CREATE (a:X)-[:E]->(b:Y)-[:E]->(c:Z)").unwrap();
assert_eq!(g.node_count(), 3);
assert_eq!(g.edge_count(), 2);
}
#[test]
fn build_graph_merge_creates_nodes() {
let g = build_graph_from_cypher("MERGE (n:Person {name: \"Charlie\"})").unwrap();
assert_eq!(g.node_count(), 1);
}
#[test]
fn build_graph_match_does_not_add_nodes() {
let g = build_graph_from_cypher("MATCH (n:Person)-[:KNOWS]->(m)").unwrap();
assert_eq!(g.node_count(), 0);
assert_eq!(g.edge_count(), 0);
}
#[test]
fn build_graph_left_directed_edge_direction() {
let g = build_graph_from_cypher("CREATE (a)<-[:LIKES]-(b)").unwrap();
assert_eq!(g.edge_count(), 1);
let e = g.edge_indices().next().unwrap();
let (src, tgt) = g.edge_endpoints(e).unwrap();
let src_data = &g[src];
let tgt_data = &g[tgt];
assert_eq!(src_data.variable.as_deref(), Some("b"));
assert_eq!(tgt_data.variable.as_deref(), Some("a"));
}
#[test]
fn build_graph_edge_with_properties() {
let g = build_graph_from_cypher(r#"CREATE (a)-[:KNOWS {since: 2020}]->(b)"#).unwrap();
let e = g.edge_indices().next().unwrap();
assert_eq!(
g[e].properties.get("since"),
Some(&CypherValue::Integer(2020))
);
}
#[test]
fn build_graph_anonymous_node() {
let g = build_graph_from_cypher("CREATE ()-[:X]->()").unwrap();
assert_eq!(g.node_count(), 2);
assert_eq!(g.edge_count(), 1);
}
#[test]
fn build_graph_node_variable_data() {
let g = build_graph_from_cypher(r#"CREATE (n:Person:Employee {age: 30})"#).unwrap();
let n = g.node_indices().next().unwrap();
let data = &g[n];
assert_eq!(data.variable.as_deref(), Some("n"));
assert!(data.labels.contains(&"Person".to_string()));
assert!(data.labels.contains(&"Employee".to_string()));
assert_eq!(data.properties.get("age"), Some(&CypherValue::Integer(30)));
}
#[derive(Debug, Clone, PartialEq)]
struct CustomNode {
variable: Option<String>,
labels: Vec<String>,
properties: HashMap<String, CypherValue>,
}
impl CypherProperties for CustomNode {
fn get(&self, name: &str) -> Option<&CypherValue> {
self.properties.get(name)
}
fn properties(&self) -> HashMap<String, CypherValue> {
self.properties.clone()
}
}
impl CypherNode for CustomNode {
fn has_label(&self, label: &str) -> bool {
self.labels.iter().any(|l| l == label)
}
fn labels(&self) -> Vec<String> {
self.labels.clone()
}
fn from_cypher(
variable: Option<String>,
labels: Vec<String>,
properties: HashMap<String, CypherValue>,
) -> Self {
Self {
variable,
labels,
properties,
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct CustomEdge {
variable: Option<String>,
rel_type: Option<String>,
properties: HashMap<String, CypherValue>,
}
impl CypherProperties for CustomEdge {
fn get(&self, name: &str) -> Option<&CypherValue> {
self.properties.get(name)
}
fn properties(&self) -> HashMap<String, CypherValue> {
self.properties.clone()
}
}
impl CypherEdge for CustomEdge {
fn has_rel_type(&self, rel_type: &str) -> bool {
self.rel_type.as_deref() == Some(rel_type)
}
fn rel_type(&self) -> Option<&str> {
self.rel_type.as_deref()
}
fn from_cypher(
variable: Option<String>,
rel_type: Option<String>,
properties: HashMap<String, CypherValue>,
) -> Self {
Self {
variable,
rel_type,
properties,
}
}
}
#[test]
fn build_graph_typed_with_custom_types() {
let g = build_graph_from_cypher_typed::<CustomNode, CustomEdge>(
r#"CREATE (a:Person {name: "Alice"})-[:KNOWS {since: 2020}]->(b:Person {name: "Bob"})"#,
)
.unwrap();
assert_eq!(g.node_count(), 2);
assert_eq!(g.edge_count(), 1);
let nodes: Vec<_> = g.node_weights().collect();
assert!(nodes.iter().any(|n| n.has_label("Person")));
assert!(
nodes
.iter()
.any(|n| n.get("name") == Some(&CypherValue::String("Alice".into())))
);
assert!(
nodes
.iter()
.any(|n| n.get("name") == Some(&CypherValue::String("Bob".into())))
);
let edge = g.edge_indices().next().unwrap();
assert_eq!(g[edge].rel_type(), Some("KNOWS"));
assert_eq!(g[edge].get("since"), Some(&CypherValue::Integer(2020)));
}