graphify_export/
cypher.rs1use std::fmt::Write;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use graphify_core::graph::KnowledgeGraph;
8use tracing::info;
9
10pub fn export_cypher(graph: &KnowledgeGraph, output_dir: &Path) -> anyhow::Result<PathBuf> {
12 let mut cypher = String::with_capacity(4096);
13
14 for node in graph.nodes() {
16 let node_type_label = format!("{:?}", node.node_type);
17 write!(
18 cypher,
19 "CREATE (n{}:{} {{id: '{}', label: '{}', source_file: '{}'",
20 sanitize_var(&node.id),
21 node_type_label,
22 cypher_escape(&node.id),
23 cypher_escape(&node.label),
24 cypher_escape(&node.source_file),
25 )
26 .unwrap();
27 if let Some(loc) = &node.source_location {
28 write!(cypher, ", source_location: '{}'", cypher_escape(loc)).unwrap();
29 }
30 if let Some(c) = node.community {
31 write!(cypher, ", community: {}", c).unwrap();
32 }
33 writeln!(cypher, "}});").unwrap();
34 }
35
36 writeln!(cypher).unwrap();
37
38 for edge in graph.edges() {
40 let rel_type = edge
41 .relation
42 .to_uppercase()
43 .replace(|c: char| !c.is_alphanumeric() && c != '_', "_");
44 writeln!(
45 cypher,
46 "CREATE (n{})-[:{} {{relation: '{}', confidence: '{:?}', confidence_score: {:.2}, source_file: '{}', weight: {:.2}}}]->(n{});",
47 sanitize_var(&edge.source),
48 rel_type,
49 cypher_escape(&edge.relation),
50 edge.confidence,
51 edge.confidence_score,
52 cypher_escape(&edge.source_file),
53 edge.weight,
54 sanitize_var(&edge.target),
55 )
56 .unwrap();
57 }
58
59 fs::create_dir_all(output_dir)?;
60 let path = output_dir.join("graph.cypher");
61 fs::write(&path, &cypher)?;
62 info!(path = %path.display(), "exported Cypher statements");
63 Ok(path)
64}
65
66fn sanitize_var(id: &str) -> String {
68 let mut out = String::with_capacity(id.len());
69 for c in id.chars() {
70 if c.is_alphanumeric() || c == '_' {
71 out.push(c);
72 } else {
73 out.push('_');
74 }
75 }
76 if out.starts_with(|c: char| c.is_ascii_digit()) {
78 out.insert(0, '_');
79 }
80 out
81}
82
83fn cypher_escape(s: &str) -> String {
84 s.replace('\\', "\\\\").replace('\'', "\\'")
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use graphify_core::confidence::Confidence;
91 use graphify_core::graph::KnowledgeGraph;
92 use graphify_core::model::{GraphEdge, GraphNode, NodeType};
93 use std::collections::HashMap;
94
95 fn sample_graph() -> KnowledgeGraph {
96 let mut kg = KnowledgeGraph::new();
97 kg.add_node(GraphNode {
98 id: "my_class".into(),
99 label: "MyClass".into(),
100 source_file: "src/main.rs".into(),
101 source_location: Some("L42".into()),
102 node_type: NodeType::Class,
103 community: Some(0),
104 extra: HashMap::new(),
105 })
106 .unwrap();
107 kg.add_node(GraphNode {
108 id: "helper".into(),
109 label: "Helper".into(),
110 source_file: "src/util.rs".into(),
111 source_location: None,
112 node_type: NodeType::Function,
113 community: None,
114 extra: HashMap::new(),
115 })
116 .unwrap();
117 kg.add_edge(GraphEdge {
118 source: "my_class".into(),
119 target: "helper".into(),
120 relation: "calls".into(),
121 confidence: Confidence::Extracted,
122 confidence_score: 1.0,
123 source_file: "src/main.rs".into(),
124 source_location: None,
125 weight: 1.0,
126 extra: HashMap::new(),
127 })
128 .unwrap();
129 kg
130 }
131
132 #[test]
133 fn export_cypher_creates_file() {
134 let dir = tempfile::tempdir().unwrap();
135 let kg = sample_graph();
136 let path = export_cypher(&kg, dir.path()).unwrap();
137 assert!(path.exists());
138
139 let content = std::fs::read_to_string(&path).unwrap();
140 assert!(content.contains("CREATE (n"));
141 assert!(content.contains("CALLS"));
142 assert!(content.contains("MyClass"));
143 }
144
145 #[test]
146 fn sanitize_var_removes_special_chars() {
147 assert_eq!(sanitize_var("my-class.foo"), "my_class_foo");
148 assert_eq!(sanitize_var("123abc"), "_123abc");
149 }
150
151 #[test]
152 fn cypher_escape_quotes() {
153 assert_eq!(cypher_escape("it's"), "it\\'s");
154 }
155}