use crate::datatypes::values::Value;
use crate::graph::schema::{CurrentSelection, DirGraph};
use crate::graph::storage::GraphRead;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::path::Path;
pub fn to_graphml(
graph: &DirGraph,
selection: Option<&CurrentSelection>,
) -> Result<String, String> {
let mut xml = String::with_capacity(64 * 1024);
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str("<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\"\n");
xml.push_str(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n");
xml.push_str(" xsi:schemaLocation=\"http://graphml.graphdrawing.org/xmlns\n");
xml.push_str(" http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd\">\n");
xml.push_str(
" <key id=\"node_type\" for=\"node\" attr.name=\"type\" attr.type=\"string\"/>\n",
);
xml.push_str(
" <key id=\"node_title\" for=\"node\" attr.name=\"title\" attr.type=\"string\"/>\n",
);
xml.push_str(" <key id=\"node_id\" for=\"node\" attr.name=\"id\" attr.type=\"string\"/>\n");
xml.push_str(" <key id=\"node_properties\" for=\"node\" attr.name=\"properties\" attr.type=\"string\"/>\n");
xml.push_str(" <key id=\"edge_type\" for=\"edge\" attr.name=\"connection_type\" attr.type=\"string\"/>\n");
xml.push_str(" <key id=\"edge_properties\" for=\"edge\" attr.name=\"properties\" attr.type=\"string\"/>\n");
xml.push_str(" <graph id=\"G\" edgedefault=\"directed\">\n");
let node_indices: Vec<_> = if let Some(sel) = selection {
let level_idx = sel.get_level_count().saturating_sub(1);
if let Some(level) = sel.get_level(level_idx) {
level.get_all_nodes()
} else {
{
let g = &graph.graph;
g.node_indices().collect()
}
}
} else {
{
let g = &graph.graph;
g.node_indices().collect()
}
};
let node_set: std::collections::HashSet<_> = node_indices.iter().copied().collect();
for &idx in &node_indices {
if let Some(node) = graph.graph.node_weight(idx) {
xml.push_str(&format!(" <node id=\"n{}\">\n", idx.index()));
xml.push_str(&format!(
" <data key=\"node_type\">{}</data>\n",
escape_xml(node.node_type_str(&graph.interner))
));
xml.push_str(&format!(
" <data key=\"node_title\">{}</data>\n",
escape_xml(&crate::datatypes::values::raw_string(&node.title()))
));
xml.push_str(&format!(
" <data key=\"node_id\">{}</data>\n",
escape_xml(&crate::datatypes::values::raw_string(&node.id()))
));
if node.property_count() > 0 {
let props_json = properties_to_json(node.property_iter(&graph.interner));
xml.push_str(&format!(
" <data key=\"node_properties\">{}</data>\n",
escape_xml(&props_json)
));
}
xml.push_str(" </node>\n");
}
}
let mut edge_id = 0;
for &source_idx in &node_indices {
for edge in {
let g = &graph.graph;
g.edges(source_idx)
} {
let target_idx = edge.target();
if node_set.contains(&target_idx) {
xml.push_str(&format!(
" <edge id=\"e{}\" source=\"n{}\" target=\"n{}\">\n",
edge_id,
source_idx.index(),
target_idx.index()
));
xml.push_str(&format!(
" <data key=\"edge_type\">{}</data>\n",
escape_xml(edge.weight().connection_type_str(&graph.interner))
));
if edge.weight().property_count() > 0 {
let props_json =
properties_to_json(edge.weight().property_iter(&graph.interner));
xml.push_str(&format!(
" <data key=\"edge_properties\">{}</data>\n",
escape_xml(&props_json)
));
}
xml.push_str(" </edge>\n");
edge_id += 1;
}
}
}
xml.push_str(" </graph>\n");
xml.push_str("</graphml>\n");
Ok(xml)
}
pub fn to_d3_json(
graph: &DirGraph,
selection: Option<&CurrentSelection>,
) -> Result<String, String> {
let node_indices: Vec<_> = if let Some(sel) = selection {
let level_idx = sel.get_level_count().saturating_sub(1);
if let Some(level) = sel.get_level(level_idx) {
level.get_all_nodes()
} else {
{
let g = &graph.graph;
g.node_indices().collect()
}
}
} else {
{
let g = &graph.graph;
g.node_indices().collect()
}
};
let node_set: std::collections::HashSet<_> = node_indices.iter().copied().collect();
let mut index_map: HashMap<usize, usize> = HashMap::with_capacity(node_indices.len());
for (pos, &idx) in node_indices.iter().enumerate() {
index_map.insert(idx.index(), pos);
}
let mut nodes_json = Vec::with_capacity(node_indices.len());
for &idx in &node_indices {
if let Some(node) = graph.graph.node_weight(idx) {
let mut obj = String::from("{");
obj.push_str(&format!("\"id\":{},", json_value(&node.id())));
obj.push_str(&format!(
"\"type\":{},",
json_string(node.node_type_str(&graph.interner))
));
obj.push_str(&format!("\"title\":{}", json_value(&node.title())));
for (key, value) in node.property_iter(&graph.interner) {
if key != "id" && key != "title" && key != "type" {
obj.push_str(&format!(",{}:{}", json_string(key), json_value(value)));
}
}
obj.push('}');
nodes_json.push(obj);
}
}
let mut links_json = Vec::new();
for &source_idx in &node_indices {
for edge in {
let g = &graph.graph;
g.edges(source_idx)
} {
let target_idx = edge.target();
if node_set.contains(&target_idx) {
if let (Some(&source_pos), Some(&target_pos)) = (
index_map.get(&source_idx.index()),
index_map.get(&target_idx.index()),
) {
let mut link = String::from("{");
link.push_str(&format!("\"source\":{},", source_pos));
link.push_str(&format!("\"target\":{},", target_pos));
link.push_str(&format!(
"\"type\":{}",
json_string(edge.weight().connection_type_str(&graph.interner))
));
for (key, value) in edge.weight().property_iter(&graph.interner) {
link.push_str(&format!(",{}:{}", json_string(key), json_value(value)));
}
link.push('}');
links_json.push(link);
}
}
}
}
let mut result = String::with_capacity(32 * 1024);
result.push_str("{\n \"nodes\": [\n ");
result.push_str(&nodes_json.join(",\n "));
result.push_str("\n ],\n \"links\": [\n ");
result.push_str(&links_json.join(",\n "));
result.push_str("\n ]\n}");
Ok(result)
}
pub fn to_gexf(graph: &DirGraph, selection: Option<&CurrentSelection>) -> Result<String, String> {
let mut xml = String::with_capacity(64 * 1024);
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str("<gexf xmlns=\"http://www.gexf.net/1.2draft\"\n");
xml.push_str(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n");
xml.push_str(" xsi:schemaLocation=\"http://www.gexf.net/1.2draft http://www.gexf.net/1.2draft/gexf.xsd\"\n");
xml.push_str(" version=\"1.2\">\n");
xml.push_str(" <meta>\n");
xml.push_str(" <creator>kglite</creator>\n");
xml.push_str(" <description>Exported from KnowledgeGraph</description>\n");
xml.push_str(" </meta>\n");
xml.push_str(" <graph mode=\"static\" defaultedgetype=\"directed\">\n");
xml.push_str(" <attributes class=\"node\">\n");
xml.push_str(" <attribute id=\"0\" title=\"type\" type=\"string\"/>\n");
xml.push_str(" <attribute id=\"1\" title=\"title\" type=\"string\"/>\n");
xml.push_str(" </attributes>\n");
xml.push_str(" <attributes class=\"edge\">\n");
xml.push_str(" <attribute id=\"0\" title=\"connection_type\" type=\"string\"/>\n");
xml.push_str(" </attributes>\n");
let node_indices: Vec<_> = if let Some(sel) = selection {
let level_idx = sel.get_level_count().saturating_sub(1);
if let Some(level) = sel.get_level(level_idx) {
level.get_all_nodes()
} else {
{
let g = &graph.graph;
g.node_indices().collect()
}
}
} else {
{
let g = &graph.graph;
g.node_indices().collect()
}
};
let node_set: std::collections::HashSet<_> = node_indices.iter().copied().collect();
xml.push_str(" <nodes>\n");
for &idx in &node_indices {
if let Some(node) = graph.graph.node_weight(idx) {
let title_str = crate::datatypes::values::raw_string(&node.title());
xml.push_str(&format!(
" <node id=\"{}\" label=\"{}\">\n",
idx.index(),
escape_xml(&title_str)
));
xml.push_str(" <attvalues>\n");
xml.push_str(&format!(
" <attvalue for=\"0\" value=\"{}\"/>\n",
escape_xml(node.node_type_str(&graph.interner))
));
xml.push_str(&format!(
" <attvalue for=\"1\" value=\"{}\"/>\n",
escape_xml(&title_str)
));
xml.push_str(" </attvalues>\n");
xml.push_str(" </node>\n");
}
}
xml.push_str(" </nodes>\n");
xml.push_str(" <edges>\n");
let mut edge_id = 0;
for &source_idx in &node_indices {
for edge in {
let g = &graph.graph;
g.edges(source_idx)
} {
let target_idx = edge.target();
if node_set.contains(&target_idx) {
xml.push_str(&format!(
" <edge id=\"{}\" source=\"{}\" target=\"{}\">\n",
edge_id,
source_idx.index(),
target_idx.index()
));
xml.push_str(" <attvalues>\n");
xml.push_str(&format!(
" <attvalue for=\"0\" value=\"{}\"/>\n",
escape_xml(edge.weight().connection_type_str(&graph.interner))
));
xml.push_str(" </attvalues>\n");
xml.push_str(" </edge>\n");
edge_id += 1;
}
}
}
xml.push_str(" </edges>\n");
xml.push_str(" </graph>\n");
xml.push_str("</gexf>\n");
Ok(xml)
}
pub fn to_csv(
graph: &DirGraph,
selection: Option<&CurrentSelection>,
) -> Result<(String, String), String> {
let node_indices: Vec<_> = if let Some(sel) = selection {
let level_idx = sel.get_level_count().saturating_sub(1);
if let Some(level) = sel.get_level(level_idx) {
level.get_all_nodes()
} else {
{
let g = &graph.graph;
g.node_indices().collect()
}
}
} else {
{
let g = &graph.graph;
g.node_indices().collect()
}
};
let node_set: std::collections::HashSet<_> = node_indices.iter().copied().collect();
let mut nodes_csv = String::from("id,type,title\n");
for &idx in &node_indices {
if let Some(node) = graph.graph.node_weight(idx) {
nodes_csv.push_str(&format!(
"{},{},{}\n",
idx.index(),
escape_csv(node.node_type_str(&graph.interner)),
escape_csv(&crate::datatypes::values::raw_string(&node.title()))
));
}
}
let mut edges_csv = String::from("source,target,type\n");
for &source_idx in &node_indices {
for edge in {
let g = &graph.graph;
g.edges(source_idx)
} {
let target_idx = edge.target();
if node_set.contains(&target_idx) {
edges_csv.push_str(&format!(
"{},{},{}\n",
source_idx.index(),
target_idx.index(),
escape_csv(edge.weight().connection_type_str(&graph.interner))
));
}
}
}
Ok((nodes_csv, edges_csv))
}
type ConnMeta = (String, String, Vec<String>, BTreeMap<String, String>);
pub struct ExportSummary {
pub output_dir: String,
pub nodes: BTreeMap<String, usize>,
pub connections: BTreeMap<String, usize>,
pub files_written: usize,
pub log_lines: Vec<String>,
}
pub fn to_csv_dir(
graph: &DirGraph,
output_dir: &str,
selection: Option<&CurrentSelection>,
parent_types: &HashMap<String, String>,
) -> Result<ExportSummary, String> {
let output = Path::new(output_dir);
let mut log_lines = Vec::new();
log_lines.push(format!("Exporting to {}...", output_dir));
let node_indices = selected_node_indices(graph, selection);
let node_set: HashSet<_> = node_indices.iter().copied().collect();
let mut nodes_by_type: BTreeMap<String, Vec<petgraph::graph::NodeIndex>> = BTreeMap::new();
for &idx in &node_indices {
if let Some(node) = graph.graph.node_weight(idx) {
nodes_by_type
.entry(node.node_type_str(&graph.interner).to_string())
.or_default()
.push(idx);
}
}
struct EdgeInfo {
source_idx: petgraph::graph::NodeIndex,
target_idx: petgraph::graph::NodeIndex,
properties: HashMap<String, Value>,
}
let mut edges_by_type: BTreeMap<String, Vec<EdgeInfo>> = BTreeMap::new();
for &source_idx in &node_indices {
for edge in {
let g = &graph.graph;
g.edges(source_idx)
} {
let target_idx = edge.target();
if node_set.contains(&target_idx) {
let w = edge.weight();
edges_by_type
.entry(w.connection_type_str(&graph.interner).to_string())
.or_default()
.push(EdgeInfo {
source_idx,
target_idx,
properties: w.properties_cloned(&graph.interner),
});
}
}
}
let nodes_dir = output.join("nodes");
let connections_dir = output.join("connections");
std::fs::create_dir_all(&nodes_dir)
.map_err(|e| format!("Failed to create nodes directory: {}", e))?;
if !edges_by_type.is_empty() {
std::fs::create_dir_all(&connections_dir)
.map_err(|e| format!("Failed to create connections directory: {}", e))?;
}
for parent in parent_types.values() {
if nodes_by_type.contains_key(parent) {
let sub_dir = nodes_dir.join(parent);
std::fs::create_dir_all(&sub_dir)
.map_err(|e| format!("Failed to create sub-node directory: {}", e))?;
}
}
let mut summary = ExportSummary {
output_dir: output_dir.to_string(),
nodes: BTreeMap::new(),
connections: BTreeMap::new(),
files_written: 0,
log_lines: Vec::new(),
};
let mut node_type_columns: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut node_type_prop_types: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
for (node_type, indices) in &nodes_by_type {
let mut prop_names: BTreeSet<String> = BTreeSet::new();
for &idx in indices {
if let Some(node) = graph.graph.node_weight(idx) {
for key in node.property_keys(&graph.interner) {
prop_names.insert(key.to_string());
}
}
}
let prop_cols: Vec<String> = prop_names.into_iter().collect();
let mut prop_types: BTreeMap<String, String> = BTreeMap::new();
for col in &prop_cols {
for &idx in indices {
if let Some(node) = graph.graph.node_weight(idx) {
if let Some(val) = node.get_property(col) {
if !matches!(*val, Value::Null) {
prop_types.insert(col.clone(), value_type_name(&val));
break;
}
}
}
}
}
let mut csv = String::with_capacity(4096);
csv.push_str("id,title");
for col in &prop_cols {
csv.push(',');
csv.push_str(&escape_csv(col));
}
csv.push('\n');
for &idx in indices {
if let Some(node) = graph.graph.node_weight(idx) {
csv.push_str(&escape_csv(&crate::datatypes::values::raw_string(
&node.id(),
)));
csv.push(',');
csv.push_str(&escape_csv(&crate::datatypes::values::raw_string(
&node.title(),
)));
for col in &prop_cols {
csv.push(',');
if let Some(val) = node.get_property(col) {
csv.push_str(&escape_csv(&crate::datatypes::values::raw_string(&val)));
}
}
csv.push('\n');
}
}
let csv_path = if let Some(parent) = parent_types.get(node_type) {
nodes_dir.join(parent).join(format!("{}.csv", node_type))
} else {
nodes_dir.join(format!("{}.csv", node_type))
};
let relative_path = csv_path
.strip_prefix(output)
.unwrap_or(&csv_path)
.to_string_lossy()
.to_string();
std::fs::write(&csv_path, &csv)
.map_err(|e| format!("Failed to write {}: {}", relative_path, e))?;
log_lines.push(format!(
" {}: {} nodes, {} properties",
relative_path,
indices.len(),
prop_cols.len()
));
summary.nodes.insert(node_type.clone(), indices.len());
summary.files_written += 1;
node_type_columns.insert(node_type.clone(), prop_cols);
node_type_prop_types.insert(node_type.clone(), prop_types);
}
let mut conn_meta: BTreeMap<String, ConnMeta> = BTreeMap::new();
for (conn_type, edges) in &edges_by_type {
let mut prop_names: BTreeSet<String> = BTreeSet::new();
for edge in edges {
for key in edge.properties.keys() {
prop_names.insert(key.clone());
}
}
let prop_cols: Vec<String> = prop_names.into_iter().collect();
let mut prop_types: BTreeMap<String, String> = BTreeMap::new();
for col in &prop_cols {
for edge in edges {
if let Some(val) = edge.properties.get(col) {
if !matches!(val, Value::Null) {
prop_types.insert(col.clone(), value_type_name(val));
break;
}
}
}
}
let source_type = edges
.first()
.and_then(|e| graph.graph.node_weight(e.source_idx))
.map(|n| n.node_type_str(&graph.interner).to_string())
.unwrap_or_default();
let target_type = edges
.first()
.and_then(|e| graph.graph.node_weight(e.target_idx))
.map(|n| n.node_type_str(&graph.interner).to_string())
.unwrap_or_default();
let mut csv = String::with_capacity(4096);
csv.push_str("source_id,source_type,target_id,target_type");
for col in &prop_cols {
csv.push(',');
csv.push_str(&escape_csv(col));
}
csv.push('\n');
for edge in edges {
let source_id = graph
.graph
.node_weight(edge.source_idx)
.map(|n| crate::datatypes::values::raw_string(&n.id()))
.unwrap_or_default();
let src_type = graph
.graph
.node_weight(edge.source_idx)
.map(|n| n.node_type_str(&graph.interner).to_string())
.unwrap_or_default();
let target_id = graph
.graph
.node_weight(edge.target_idx)
.map(|n| crate::datatypes::values::raw_string(&n.id()))
.unwrap_or_default();
let tgt_type = graph
.graph
.node_weight(edge.target_idx)
.map(|n| n.node_type_str(&graph.interner).to_string())
.unwrap_or_default();
csv.push_str(&escape_csv(&source_id));
csv.push(',');
csv.push_str(&escape_csv(&src_type));
csv.push(',');
csv.push_str(&escape_csv(&target_id));
csv.push(',');
csv.push_str(&escape_csv(&tgt_type));
for col in &prop_cols {
csv.push(',');
if let Some(val) = edge.properties.get(col) {
csv.push_str(&escape_csv(&crate::datatypes::values::raw_string(val)));
}
}
csv.push('\n');
}
let csv_path = connections_dir.join(format!("{}.csv", conn_type));
let relative_path = csv_path
.strip_prefix(output)
.unwrap_or(&csv_path)
.to_string_lossy()
.to_string();
std::fs::write(&csv_path, &csv)
.map_err(|e| format!("Failed to write {}: {}", relative_path, e))?;
log_lines.push(format!(" {}: {} edges", relative_path, edges.len()));
summary.connections.insert(conn_type.clone(), edges.len());
summary.files_written += 1;
conn_meta.insert(
conn_type.clone(),
(source_type, target_type, prop_cols, prop_types),
);
}
let blueprint = build_blueprint(
&nodes_by_type,
&node_type_columns,
&node_type_prop_types,
parent_types,
&conn_meta,
output,
);
let blueprint_path = output.join("blueprint.json");
std::fs::write(&blueprint_path, &blueprint)
.map_err(|e| format!("Failed to write blueprint.json: {}", e))?;
log_lines.push(" blueprint.json".to_string());
summary.files_written += 1;
let total_nodes: usize = summary.nodes.values().sum();
let total_edges: usize = summary.connections.values().sum();
log_lines.push(format!(
"Done: {} nodes, {} edges, {} files written",
total_nodes, total_edges, summary.files_written
));
summary.log_lines = log_lines;
Ok(summary)
}
fn selected_node_indices(
graph: &DirGraph,
selection: Option<&CurrentSelection>,
) -> Vec<petgraph::graph::NodeIndex> {
let g = &graph.graph;
if let Some(sel) = selection {
let level_idx = sel.get_level_count().saturating_sub(1);
if let Some(level) = sel.get_level(level_idx) {
level.get_all_nodes()
} else {
g.node_indices().collect()
}
} else {
g.node_indices().collect()
}
}
fn value_type_name(value: &Value) -> String {
match value {
Value::String(_) => "string".to_string(),
Value::Int64(_) => "int".to_string(),
Value::Float64(_) => "float".to_string(),
Value::Boolean(_) => "bool".to_string(),
Value::DateTime(_) => "date".to_string(),
Value::UniqueId(_) => "int".to_string(),
Value::Point { .. } => "string".to_string(), Value::Duration { .. } => "string".to_string(),
Value::Null => "string".to_string(),
Value::NodeRef(_) => "int".to_string(),
Value::List(_)
| Value::Map(_)
| Value::Node(_)
| Value::Relationship(_)
| Value::Path(_) => "string".to_string(),
}
}
fn build_blueprint(
nodes_by_type: &BTreeMap<String, Vec<petgraph::graph::NodeIndex>>,
_node_type_columns: &BTreeMap<String, Vec<String>>,
node_type_prop_types: &BTreeMap<String, BTreeMap<String, String>>,
parent_types: &HashMap<String, String>,
conn_meta: &BTreeMap<String, ConnMeta>,
_output_dir: &Path,
) -> String {
let mut json = String::with_capacity(4096);
json.push_str("{\n \"settings\": {\n \"root\": \".\"\n },\n \"nodes\": {");
let mut first_node = true;
for node_type in nodes_by_type.keys() {
if !first_node {
json.push(',');
}
first_node = false;
let csv_rel = if let Some(parent) = parent_types.get(node_type) {
format!("nodes/{}/{}.csv", parent, node_type)
} else {
format!("nodes/{}.csv", node_type)
};
json.push_str(&format!("\n {}: {{\n", json_string(node_type)));
json.push_str(&format!(" \"csv\": {},\n", json_string(&csv_rel)));
json.push_str(" \"pk\": \"id\",\n");
json.push_str(" \"title\": \"title\"");
if let Some(prop_types) = node_type_prop_types.get(node_type) {
if !prop_types.is_empty() {
json.push_str(",\n \"properties\": {");
let mut first_prop = true;
for (col, typ) in prop_types {
if !first_prop {
json.push(',');
}
first_prop = false;
json.push_str(&format!(
"\n {}: {}",
json_string(col),
json_string(typ)
));
}
json.push_str("\n }");
}
}
if let Some(parent) = parent_types.get(node_type) {
json.push_str(&format!(",\n \"parent\": {}", json_string(parent)));
}
let node_conns: Vec<_> = conn_meta
.iter()
.filter(|(_, (src, _, _, _))| src == node_type)
.collect();
if !node_conns.is_empty() {
json.push_str(",\n \"connections\": {\n \"junction_edges\": {");
let mut first_conn = true;
for (conn_type, (_, target_type, prop_cols, _prop_types)) in &node_conns {
if !first_conn {
json.push(',');
}
first_conn = false;
let conn_csv = format!("connections/{}.csv", conn_type);
json.push_str(&format!(
"\n {}: {{\n \"csv\": {},\n \"source_fk\": \"source_id\",\n \"target\": {},\n \"target_fk\": \"target_id\"",
json_string(conn_type),
json_string(&conn_csv),
json_string(target_type)
));
if !prop_cols.is_empty() {
json.push_str(",\n \"properties\": [");
let mut first_p = true;
for p in prop_cols.iter() {
if !first_p {
json.push_str(", ");
}
first_p = false;
json.push_str(&json_string(p));
}
json.push(']');
}
json.push_str("\n }");
}
json.push_str("\n }\n }");
}
json.push_str("\n }");
}
json.push_str("\n }\n}\n");
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&json) {
if let Ok(pretty) = serde_json::to_string_pretty(&parsed) {
return pretty;
}
}
json
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn escape_csv(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}
fn json_string(s: &str) -> String {
format!(
"\"{}\"",
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
)
}
fn json_value(value: &Value) -> String {
match value {
Value::String(s) => json_string(s),
Value::Int64(n) => n.to_string(),
Value::Float64(f) => {
if f.is_nan() || f.is_infinite() {
"null".to_string()
} else {
f.to_string()
}
}
Value::Boolean(b) => b.to_string(),
Value::DateTime(dt) => json_string(&dt.to_string()),
Value::UniqueId(id) => id.to_string(),
Value::Point { lat, lon } => format!("{{\"lat\":{},\"lon\":{}}}", lat, lon),
Value::Duration {
months,
days,
seconds,
} => format!(
"{{\"months\":{},\"days\":{},\"seconds\":{}}}",
months, days, seconds
),
Value::Null => "null".to_string(),
Value::NodeRef(idx) => idx.to_string(),
Value::List(_)
| Value::Map(_)
| Value::Node(_)
| Value::Relationship(_)
| Value::Path(_) => serde_json::to_string(value).unwrap_or_else(|_| "null".to_string()),
}
}
fn properties_to_json<'a>(properties: impl Iterator<Item = (&'a str, &'a Value)>) -> String {
let pairs: Vec<String> = properties
.map(|(k, v)| format!("{}:{}", json_string(k), json_value(v)))
.collect();
format!("{{{}}}", pairs.join(","))
}