use crate::datatypes::values::Value;
use std::collections::HashMap;
pub fn json_object_to_value_map(
map: &serde_json::Map<String, serde_json::Value>,
) -> HashMap<String, Value> {
map.iter()
.map(|(k, v)| (k.clone(), json_value_to_kglite_value(v)))
.collect()
}
pub fn json_value_to_kglite_value(v: &serde_json::Value) -> Value {
match v {
serde_json::Value::Null => Value::Null,
serde_json::Value::Bool(b) => Value::Boolean(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int64(i)
} else if let Some(f) = n.as_f64() {
Value::Float64(f)
} else {
Value::Null
}
}
serde_json::Value::String(s) => Value::String(s.clone()),
serde_json::Value::Array(items) => {
Value::List(items.iter().map(json_value_to_kglite_value).collect())
}
serde_json::Value::Object(map) => Value::Map(
map.iter()
.map(|(k, v)| (k.clone(), json_value_to_kglite_value(v)))
.collect(),
),
}
}
pub fn kglite_value_to_json(v: &Value) -> serde_json::Value {
use serde_json::Value as J;
match v {
Value::Null => J::Null,
Value::Boolean(b) => J::Bool(*b),
Value::Int64(i) => J::Number((*i).into()),
Value::Float64(f) => serde_json::Number::from_f64(*f)
.map(J::Number)
.unwrap_or(J::Null),
Value::String(s) => J::String(s.clone()),
Value::List(items) => J::Array(items.iter().map(kglite_value_to_json).collect()),
Value::Map(m) => J::Object(
m.iter()
.map(|(k, v)| (k.clone(), kglite_value_to_json(v)))
.collect(),
),
Value::UniqueId(u) => J::Number((*u).into()),
Value::NodeRef(idx) => J::Number((*idx).into()),
Value::DateTime(d) => J::String(d.format("%Y-%m-%d").to_string()),
Value::Timestamp(dt) => J::String(dt.format("%Y-%m-%dT%H:%M:%S").to_string()),
Value::Point { lat, lon } => J::Object(
[
("latitude".to_string(), json_number(*lat)),
("longitude".to_string(), json_number(*lon)),
]
.into_iter()
.collect(),
),
Value::Duration {
months,
days,
seconds,
} => J::Object(
[
("months".to_string(), J::Number((*months).into())),
("days".to_string(), J::Number((*days).into())),
("seconds".to_string(), J::Number((*seconds).into())),
]
.into_iter()
.collect(),
),
Value::Node(node) => node_to_json(node),
Value::Relationship(rel) => rel_to_json(rel),
Value::Path(path) => J::Object(
[
(
"nodes".to_string(),
J::Array(path.nodes.iter().map(node_to_json).collect()),
),
(
"relationships".to_string(),
J::Array(path.rels.iter().map(rel_to_json).collect()),
),
]
.into_iter()
.collect(),
),
}
}
fn json_number(f: f64) -> serde_json::Value {
serde_json::Number::from_f64(f)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null)
}
fn properties_to_json(props: &std::collections::BTreeMap<String, Value>) -> serde_json::Value {
serde_json::Value::Object(
props
.iter()
.map(|(k, v)| (k.clone(), kglite_value_to_json(v)))
.collect(),
)
}
fn node_to_json(node: &crate::datatypes::values::NodeValue) -> serde_json::Value {
use serde_json::Value as J;
J::Object(
[
("id".to_string(), J::Number(node.id.into())),
(
"labels".to_string(),
J::Array(node.labels.iter().map(|l| J::String(l.clone())).collect()),
),
(
"properties".to_string(),
properties_to_json(&node.properties),
),
]
.into_iter()
.collect(),
)
}
fn rel_to_json(rel: &crate::datatypes::values::RelValue) -> serde_json::Value {
use serde_json::Value as J;
J::Object(
[
("id".to_string(), J::Number(rel.id.into())),
("start".to_string(), J::Number(rel.start_id.into())),
("end".to_string(), J::Number(rel.end_id.into())),
("type".to_string(), J::String(rel.rel_type.clone())),
(
"properties".to_string(),
properties_to_json(&rel.properties),
),
]
.into_iter()
.collect(),
)
}
#[cfg(test)]
#[allow(clippy::approx_constant)]
mod tests {
use super::*;
#[test]
fn null_roundtrip() {
assert_eq!(
json_value_to_kglite_value(&serde_json::json!(null)),
Value::Null
);
}
#[test]
fn bool_roundtrip() {
assert_eq!(
json_value_to_kglite_value(&serde_json::json!(true)),
Value::Boolean(true)
);
assert_eq!(
json_value_to_kglite_value(&serde_json::json!(false)),
Value::Boolean(false)
);
}
#[test]
fn integer_number() {
assert_eq!(
json_value_to_kglite_value(&serde_json::json!(42)),
Value::Int64(42)
);
assert_eq!(
json_value_to_kglite_value(&serde_json::json!(-7)),
Value::Int64(-7)
);
}
#[test]
fn float_number() {
match json_value_to_kglite_value(&serde_json::json!(3.14)) {
Value::Float64(f) => assert!((f - 3.14).abs() < 1e-9),
other => panic!("expected Float64, got {other:?}"),
}
}
#[test]
fn string_roundtrip() {
assert_eq!(
json_value_to_kglite_value(&serde_json::json!("hello")),
Value::String("hello".to_string())
);
}
#[test]
fn array_maps_to_list() {
let v = serde_json::json!([1, "two", null, true]);
assert_eq!(
json_value_to_kglite_value(&v),
Value::List(vec![
Value::Int64(1),
Value::String("two".to_string()),
Value::Null,
Value::Boolean(true),
])
);
}
#[test]
fn object_maps_to_map() {
let v = serde_json::json!({"a": 1, "b": "x"});
let mut expected = std::collections::BTreeMap::new();
expected.insert("a".to_string(), Value::Int64(1));
expected.insert("b".to_string(), Value::String("x".to_string()));
assert_eq!(json_value_to_kglite_value(&v), Value::Map(expected));
}
#[test]
fn nested_array_of_objects() {
let v = serde_json::json!([{"id": 1}, {"id": 2}]);
match json_value_to_kglite_value(&v) {
Value::List(items) => {
assert_eq!(items.len(), 2);
match &items[0] {
Value::Map(m) => assert_eq!(m.get("id"), Some(&Value::Int64(1))),
other => panic!("expected Map, got {other:?}"),
}
}
other => panic!("expected List, got {other:?}"),
}
}
#[test]
fn value_to_json_natural_scalars() {
assert_eq!(kglite_value_to_json(&Value::Int64(2)), serde_json::json!(2));
assert_eq!(
kglite_value_to_json(&Value::String("x".into())),
serde_json::json!("x")
);
assert_eq!(
kglite_value_to_json(&Value::Boolean(true)),
serde_json::json!(true)
);
assert_eq!(kglite_value_to_json(&Value::Null), serde_json::Value::Null);
}
#[test]
fn value_to_json_natural_nested_is_untagged() {
let mut m = std::collections::BTreeMap::new();
m.insert("id".to_string(), Value::Int64(7));
let v = Value::List(vec![Value::Int64(1), Value::Map(m)]);
assert_eq!(kglite_value_to_json(&v), serde_json::json!([1, {"id": 7}]));
}
#[test]
fn value_to_json_is_inverse_of_inbound() {
let j = serde_json::json!({"rows": [{"id": 1}, {"id": 2}]});
let back = kglite_value_to_json(&json_value_to_kglite_value(&j));
assert_eq!(back, j);
}
fn sample_node() -> crate::datatypes::values::NodeValue {
let mut props = std::collections::BTreeMap::new();
props.insert("name".to_string(), Value::String("Ada".into()));
props.insert("rank".to_string(), Value::Int64(1));
crate::datatypes::values::NodeValue {
id: 7,
labels: vec!["Person".to_string()],
properties: props,
}
}
fn sample_rel() -> crate::datatypes::values::RelValue {
let mut props = std::collections::BTreeMap::new();
props.insert("weight".to_string(), Value::Int64(3));
crate::datatypes::values::RelValue {
id: 11,
start_id: 7,
end_id: 8,
rel_type: "KNOWS".to_string(),
properties: props,
}
}
#[test]
fn value_to_json_node_is_structured() {
assert_eq!(
kglite_value_to_json(&Value::Node(Box::new(sample_node()))),
serde_json::json!({
"id": 7,
"labels": ["Person"],
"properties": {"name": "Ada", "rank": 1},
})
);
}
#[test]
fn value_to_json_relationship_is_structured() {
assert_eq!(
kglite_value_to_json(&Value::Relationship(Box::new(sample_rel()))),
serde_json::json!({
"id": 11,
"start": 7,
"end": 8,
"type": "KNOWS",
"properties": {"weight": 3},
})
);
}
#[test]
fn value_to_json_path_is_structured() {
let path = crate::datatypes::values::PathValue {
nodes: vec![sample_node()],
rels: vec![sample_rel()],
};
let json = kglite_value_to_json(&Value::Path(Box::new(path)));
assert_eq!(json["nodes"][0]["id"], serde_json::json!(7));
assert_eq!(json["relationships"][0]["type"], serde_json::json!("KNOWS"));
assert_eq!(json["nodes"][0]["properties"]["name"], "Ada");
}
#[test]
fn value_to_json_temporal_and_spatial_are_natural() {
use chrono::{NaiveDate, NaiveTime};
let date = NaiveDate::from_ymd_opt(2024, 3, 9).unwrap();
assert_eq!(
kglite_value_to_json(&Value::DateTime(date)),
serde_json::json!("2024-03-09")
);
let stamp = date.and_time(NaiveTime::from_hms_opt(14, 30, 5).unwrap());
assert_eq!(
kglite_value_to_json(&Value::Timestamp(stamp)),
serde_json::json!("2024-03-09T14:30:05")
);
assert_eq!(
kglite_value_to_json(&Value::Point {
lat: 59.9,
lon: 10.7
}),
serde_json::json!({"latitude": 59.9, "longitude": 10.7})
);
assert_eq!(
kglite_value_to_json(&Value::Duration {
months: 1,
days: 2,
seconds: 30,
}),
serde_json::json!({"months": 1, "days": 2, "seconds": 30})
);
}
#[test]
fn value_to_json_id_variants_are_numbers() {
assert_eq!(
kglite_value_to_json(&Value::UniqueId(42)),
serde_json::json!(42)
);
assert_eq!(
kglite_value_to_json(&Value::NodeRef(5)),
serde_json::json!(5)
);
}
#[test]
fn no_value_variant_renders_as_a_debug_string() {
use chrono::NaiveDate;
let date = NaiveDate::from_ymd_opt(2024, 3, 9).unwrap();
let every_variant = [
Value::Null,
Value::Boolean(true),
Value::Int64(1),
Value::Float64(1.5),
Value::String("s".into()),
Value::UniqueId(1),
Value::NodeRef(1),
Value::DateTime(date),
Value::Timestamp(date.and_hms_opt(0, 0, 0).unwrap()),
Value::Point { lat: 1.0, lon: 2.0 },
Value::Duration {
months: 1,
days: 1,
seconds: 1,
},
Value::List(vec![Value::Int64(1)]),
Value::Map(std::collections::BTreeMap::new()),
Value::Node(Box::new(sample_node())),
Value::Relationship(Box::new(sample_rel())),
Value::Path(Box::new(crate::datatypes::values::PathValue {
nodes: vec![sample_node()],
rels: vec![],
})),
];
for value in &every_variant {
let rendered = kglite_value_to_json(value).to_string();
for constructor in [
"Node(",
"Relationship(",
"Path(",
"DateTime(",
"Timestamp(",
"Duration ",
"UniqueId(",
"NodeRef(",
"Point ",
"Int64(",
] {
assert!(
!rendered.contains(constructor),
"{value:?} leaked a Debug rendering: {rendered}"
);
}
}
}
}