use mushroomdb::{
default_max_edges, json_to_value, tagged_json, EdgeEvent, EdgeHistoryEvent, EdgeInfo,
HistoryChange, HistoryEntry, HistoryResult, MaskedEdge, NodeInfo, ResultSet, RuleDef, Value,
};
use serde_json::{json, Value as Js};
use std::collections::BTreeMap;
pub(crate) fn value_to_json(v: &Value) -> Js {
match v {
Value::Int(i) => json!(i),
Value::Float(f) => serde_json::Number::from_f64(*f)
.map(Js::Number)
.unwrap_or(Js::Null),
Value::Str(s) => json!(s),
Value::Bool(b) => json!(b),
Value::List(xs) => Js::Array(xs.iter().map(value_to_json).collect()),
Value::Map(m) => {
let obj: serde_json::Map<String, Js> = m
.iter()
.map(|(k, v)| (k.clone(), value_to_json(v)))
.collect();
Js::Object(obj)
}
Value::Date(_)
| Value::LocalDateTime(_)
| Value::DateTime { .. }
| Value::Duration { .. }
| Value::Binary(_) => tagged_json(v).ok().flatten().unwrap_or(Js::Null),
Value::Null => Js::Null,
}
}
pub(crate) fn node_info_json(info: &NodeInfo) -> Js {
let props: serde_json::Map<String, Js> = info
.props
.iter()
.map(|(k, v)| (k.clone(), value_to_json(v)))
.collect();
json!({
"key": info.key,
"label": info.label,
"props": props,
})
}
pub(crate) fn node_edges_json(edges: &[EdgeInfo]) -> Js {
json!({
"edges": edges.iter().map(|e| json!({
"edge_type": e.edge_type,
"src_key": e.src_key,
"dst_key": e.dst_key,
"derived": e.derived,
})).collect::<Vec<_>>()
})
}
pub(crate) fn stub_node_json(key: &str) -> Js {
json!({"key": key, "restricted": true})
}
pub(crate) fn masked_edges_json(edges: &[MaskedEdge]) -> Js {
let items: Vec<Js> = edges
.iter()
.map(|e| {
let src = if e.src_restricted {
json!({"key": e.src_key, "restricted": true})
} else {
json!(e.src_key)
};
let dst = if e.dst_restricted {
json!({"key": e.dst_key, "restricted": true})
} else {
json!(e.dst_key)
};
json!({
"edge_type": e.edge_type,
"src_key": src,
"dst_key": dst,
"derived": e.derived,
})
})
.collect();
json!({"edges": items})
}
pub(crate) fn result_set_json(rs: &ResultSet) -> Js {
let columns: Vec<&str> = rs.columns().iter().map(String::as_str).collect();
let rows: Vec<Vec<Js>> = (0..rs.len())
.map(|i| {
rs.row(i)
.iter()
.map(|cell| cell.as_ref().map(value_to_json).unwrap_or(Js::Null))
.collect()
})
.collect();
json!({ "columns": columns, "rows": rows })
}
pub(crate) fn params_from_json(v: Option<&Js>) -> Result<BTreeMap<String, Value>, String> {
let Some(v) = v else {
return Ok(BTreeMap::new());
};
let obj = v
.as_object()
.ok_or_else(|| "params must be an object".to_string())?;
let mut out = BTreeMap::new();
for (k, val) in obj {
let mapped = param_from_json(val).map_err(|e| format!("param {k:?}: {e}"))?;
out.insert(k.clone(), mapped);
}
Ok(out)
}
fn param_from_json(v: &Js) -> Result<Value, String> {
match v {
Js::Null => Ok(Value::Null),
Js::Array(items) => items
.iter()
.enumerate()
.map(|(i, item)| param_from_json(item).map_err(|e| format!("item {i}: {e}")))
.collect::<Result<Vec<_>, _>>()
.map(Value::List),
Js::Object(obj) => {
if obj.len() == 1 && obj.keys().all(|k| k.starts_with('$')) {
if let Some(typed) = json_to_value(v.clone())? {
if !matches!(typed, Value::Map(_)) {
return Ok(typed);
}
}
}
obj.iter()
.map(|(k, item)| {
param_from_json(item)
.map(|val| (k.clone(), val))
.map_err(|e| format!("key {k:?}: {e}"))
})
.collect::<Result<BTreeMap<_, _>, _>>()
.map(Value::Map)
}
scalar => {
json_to_value(scalar.clone())?.ok_or_else(|| "value cannot be a parameter".to_string())
}
}
}
pub(crate) fn parse_ingest_edges(v: &Js) -> Result<Vec<(String, String, String)>, String> {
let arr = v
.as_array()
.ok_or_else(|| "edges must be an array".to_string())?;
let mut out = Vec::with_capacity(arr.len());
for (i, item) in arr.iter().enumerate() {
let obj = item
.as_object()
.ok_or_else(|| format!("edges[{i}] must be an object"))?;
let edge_type = obj
.get("edge_type")
.and_then(Js::as_str)
.ok_or_else(|| format!("edges[{i}] missing edge_type"))?;
let src = obj
.get("src")
.and_then(Js::as_str)
.ok_or_else(|| format!("edges[{i}] missing src"))?;
let dst = obj
.get("dst")
.and_then(Js::as_str)
.ok_or_else(|| format!("edges[{i}] missing dst"))?;
out.push((edge_type.to_string(), src.to_string(), dst.to_string()));
}
Ok(out)
}
pub(crate) fn history_change_json(change: &HistoryChange) -> Js {
match change {
HistoryChange::NodeInserted { label } => json!({"type": "NodeInserted", "label": label}),
HistoryChange::PropSet { field, value } => json!({
"type": "PropSet",
"field": field,
"value": value_to_json(value),
}),
HistoryChange::PropRemoved { field } => json!({"type": "PropRemoved", "field": field}),
HistoryChange::EdgeAdded {
edge_type,
other,
outgoing,
} => json!({
"type": "EdgeAdded",
"edge_type": edge_type,
"other": other,
"outgoing": outgoing,
}),
HistoryChange::EdgeRemoved {
edge_type,
other,
outgoing,
} => json!({
"type": "EdgeRemoved",
"edge_type": edge_type,
"other": other,
"outgoing": outgoing,
}),
HistoryChange::NodeDeleted => json!({"type": "NodeDeleted"}),
}
}
pub(crate) fn history_entry_json(entry: &HistoryEntry) -> Js {
json!({
"commit": entry.commit,
"change": history_change_json(&entry.change),
})
}
pub(crate) fn edge_history_result_json(
a: &str,
b: &str,
result: &HistoryResult<EdgeHistoryEvent>,
) -> Js {
let events: Vec<Js> = result
.items
.iter()
.map(|ev| {
let event_str = match ev.event {
EdgeEvent::Added => "Added",
EdgeEvent::Retracted => "Retracted",
};
json!({
"edge_type": ev.edge_type,
"commit": ev.commit,
"event": event_str,
"rule": ev.rule,
})
})
.collect();
json!({
"a": a,
"b": b,
"events": events,
"total_commits": result.total_commits,
"horizon": result.horizon,
})
}
pub(crate) fn node_history_json(key: &str, result: &HistoryResult<HistoryEntry>) -> Js {
let history: Vec<Js> = result.items.iter().map(history_entry_json).collect();
json!({
"key": key,
"history": history,
"total_commits": result.total_commits,
"horizon": result.horizon,
})
}
pub(crate) fn namespace_arg(v: Option<&Js>) -> Result<Option<String>, String> {
match v {
None | Some(Js::Null) => Ok(None),
Some(Js::String(s)) if mushroomdb::valid_namespace(s) => Ok(Some(s.clone())),
Some(Js::String(s)) => Err(format!(
"namespace {s:?} is not a valid namespace name — 1 to {} characters of [A-Za-z0-9_.-]",
mushroomdb::NS_MAX_LEN
)),
Some(_) => Err("namespace must be a string naming a namespace".into()),
}
}
pub(crate) fn stamp_namespace(
rows: &mut [BTreeMap<String, Value>],
namespace: Option<&str>,
) -> Result<(), String> {
let Some(ns) = namespace else {
return Ok(());
};
for (i, row) in rows.iter_mut().enumerate() {
stamp_namespace_row(row, ns).map_err(|e| format!("row {i}: {e}"))?;
}
Ok(())
}
pub(crate) fn stamp_namespace_row(
row: &mut BTreeMap<String, Value>,
namespace: &str,
) -> Result<(), String> {
match row.get(mushroomdb::NS_PROP) {
Some(Value::Str(s)) if s == namespace => Ok(()),
Some(existing) => Err(format!(
"ns is {existing:?} but the call names namespace {namespace:?}; a node is created in \
one namespace, so pass one or the other"
)),
None => {
row.insert(
mushroomdb::NS_PROP.to_string(),
Value::Str(namespace.to_string()),
);
Ok(())
}
}
}
pub(crate) fn rule_def_from_json(v: Js) -> Result<RuleDef, String> {
let mut def: RuleDef = serde_json::from_value(v).map_err(|e| e.to_string())?;
if def.max_edges.is_none() {
def.max_edges = Some(default_max_edges(&def.predicate));
}
if def.weight_prop.is_none() {
def.weight_prop = Some("weight".into());
}
Ok(def)
}
#[cfg(test)]
mod tests {
use super::value_to_json;
use mushroomdb::{json_to_value, Value};
use serde_json::json;
#[test]
fn test_value_json_roundtrip() {
let cases = [
(Value::Date(19_737), json!({"$date": "2024-01-15"})),
(
Value::LocalDateTime(1_705_322_096_789_012),
json!({"$ldt": "2024-01-15T12:34:56.789012"}),
),
(
Value::DateTime {
micros_utc: 1_705_302_296_789_012,
offset_seconds: 19_800,
},
json!({"$dt": "2024-01-15T12:34:56.789012+05:30"}),
),
(
Value::Duration {
months: 14,
days: 3,
nanos: 14_706_789_000_000,
},
json!({"$dur": "P1Y2M3DT4H5M6.789S"}),
),
(
Value::Binary(vec![0xDE, 0xAD, 0xBE, 0xEF]),
json!({"$binary": "3q2+7w=="}),
),
];
for (value, wire) in cases {
assert_eq!(value_to_json(&value), wire);
assert_eq!(json_to_value(wire), Ok(Some(value)));
}
let list = Value::List(vec![Value::Date(0), Value::Binary(vec![])]);
assert_eq!(
value_to_json(&list),
json!([{"$date": "1970-01-01"}, {"$binary": ""}])
);
}
}