use std::collections::BTreeMap;
use lora_executor::LoraValue;
use lora_store::{
LoraBinary, LoraDate, LoraDateTime, LoraDuration, LoraLocalDateTime, LoraLocalTime, LoraPoint,
LoraTime, LoraVector, RawCoordinate, VectorCoordinateType, VectorValues,
};
use serde_json::Value as J;
pub fn lora_value_to_json(value: &LoraValue) -> J {
match value {
LoraValue::Null => J::Null,
LoraValue::Bool(b) => J::Bool(*b),
LoraValue::Int(i) => J::Number((*i).into()),
LoraValue::Float(f) => serde_json::Number::from_f64(*f)
.map(J::Number)
.unwrap_or(J::Null),
LoraValue::String(s) => J::String(s.clone()),
LoraValue::Binary(b) => binary_to_json(b),
LoraValue::List(items) => J::Array(items.iter().map(lora_value_to_json).collect()),
LoraValue::Map(m) => J::Object(
m.iter()
.map(|(k, v)| (k.clone(), lora_value_to_json(v)))
.collect(),
),
LoraValue::Node(id) => serde_json::json!({
"kind": "node",
"id": *id as i64,
"labels": J::Array(vec![]),
"properties": J::Object(Default::default()),
}),
LoraValue::Relationship(id) => serde_json::json!({
"kind": "relationship",
"id": *id as i64,
}),
LoraValue::Path(p) => serde_json::json!({
"kind": "path",
"nodes": p.nodes.iter().map(|n| *n as i64).collect::<Vec<_>>(),
"rels": p.rels.iter().map(|n| *n as i64).collect::<Vec<_>>(),
}),
LoraValue::Date(d) => serde_json::json!({ "kind": "date", "iso": d.to_string() }),
LoraValue::Time(t) => serde_json::json!({ "kind": "time", "iso": t.to_string() }),
LoraValue::LocalTime(t) => serde_json::json!({ "kind": "localtime", "iso": t.to_string() }),
LoraValue::DateTime(dt) => serde_json::json!({ "kind": "datetime", "iso": dt.to_string() }),
LoraValue::LocalDateTime(dt) => {
serde_json::json!({ "kind": "localdatetime", "iso": dt.to_string() })
}
LoraValue::Duration(d) => serde_json::json!({ "kind": "duration", "iso": d.to_string() }),
LoraValue::Point(p) => point_to_json(p),
LoraValue::Vector(v) => vector_to_json(v),
}
}
fn binary_to_json(b: &LoraBinary) -> J {
serde_json::json!({
"kind": "binary",
"length": b.len(),
"segments": b.segments(),
})
}
fn vector_to_json(v: &LoraVector) -> J {
let values: J = match &v.values {
VectorValues::Float64(vs) => J::Array(
vs.iter()
.map(|x| {
serde_json::Number::from_f64(*x)
.map(J::Number)
.unwrap_or(J::Null)
})
.collect(),
),
VectorValues::Float32(vs) => J::Array(
vs.iter()
.map(|x| {
serde_json::Number::from_f64(*x as f64)
.map(J::Number)
.unwrap_or(J::Null)
})
.collect(),
),
VectorValues::Integer64(vs) => J::Array(vs.iter().map(|x| serde_json::json!(*x)).collect()),
VectorValues::Integer32(vs) => {
J::Array(vs.iter().map(|x| serde_json::json!(*x as i64)).collect())
}
VectorValues::Integer16(vs) => {
J::Array(vs.iter().map(|x| serde_json::json!(*x as i64)).collect())
}
VectorValues::Integer8(vs) => {
J::Array(vs.iter().map(|x| serde_json::json!(*x as i64)).collect())
}
};
serde_json::json!({
"kind": "vector",
"dimension": v.dimension,
"coordinateType": v.coordinate_type().as_str(),
"values": values,
})
}
fn point_to_json(p: &LoraPoint) -> J {
let mut obj = serde_json::Map::with_capacity(7);
obj.insert("kind".into(), J::String("point".into()));
obj.insert("srid".into(), serde_json::json!(p.srid));
obj.insert("crs".into(), J::String(p.crs_name().into()));
obj.insert("x".into(), serde_json::json!(p.x));
obj.insert("y".into(), serde_json::json!(p.y));
if let Some(z) = p.z {
obj.insert("z".into(), serde_json::json!(z));
}
if p.is_geographic() {
obj.insert("longitude".into(), serde_json::json!(p.longitude()));
obj.insert("latitude".into(), serde_json::json!(p.latitude()));
if let Some(h) = p.height() {
obj.insert("height".into(), serde_json::json!(h));
}
}
J::Object(obj)
}
pub fn lora_value_from_json(value: J) -> Result<LoraValue, String> {
match value {
J::Null => Ok(LoraValue::Null),
J::Bool(b) => Ok(LoraValue::Bool(b)),
J::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(LoraValue::Int(i))
} else if let Some(f) = n.as_f64() {
Ok(LoraValue::Float(f))
} else {
Err("unsupported numeric value".into())
}
}
J::String(s) => Ok(LoraValue::String(s)),
J::Array(items) => Ok(LoraValue::List(
items
.into_iter()
.map(lora_value_from_json)
.collect::<Result<Vec<_>, _>>()?,
)),
J::Object(obj) => {
if let Some(J::String(kind)) = obj.get("kind") {
match kind.as_str() {
"date" => return Ok(LoraValue::Date(parse_iso::<LoraDate>(&obj, "date")?)),
"time" => return Ok(LoraValue::Time(parse_iso::<LoraTime>(&obj, "time")?)),
"localtime" => {
return Ok(LoraValue::LocalTime(parse_iso::<LoraLocalTime>(
&obj,
"localtime",
)?))
}
"datetime" => {
return Ok(LoraValue::DateTime(parse_iso::<LoraDateTime>(
&obj, "datetime",
)?))
}
"localdatetime" => {
return Ok(LoraValue::LocalDateTime(parse_iso::<LoraLocalDateTime>(
&obj,
"localdatetime",
)?))
}
"duration" => {
return Ok(LoraValue::Duration(parse_iso::<LoraDuration>(
&obj, "duration",
)?))
}
"point" => return Ok(LoraValue::Point(point_from_obj(&obj)?)),
"vector" => return Ok(LoraValue::Vector(vector_from_obj(&obj)?)),
"binary" | "blob" => return Ok(LoraValue::Binary(binary_from_obj(&obj)?)),
"node" | "relationship" | "path" => {
return Err(format!("cannot import a hydrated {kind} value directly"));
}
_ => {}
}
}
let mut map = BTreeMap::new();
for (k, v) in obj {
map.insert(k, lora_value_from_json(v)?);
}
Ok(LoraValue::Map(map))
}
}
}
trait ParseIso: Sized {
fn parse_iso(s: &str) -> Result<Self, String>;
}
macro_rules! impl_parse_iso {
($ty:ty) => {
impl ParseIso for $ty {
fn parse_iso(s: &str) -> Result<Self, String> {
<$ty>::parse(s).map_err(|e| e.to_string())
}
}
};
}
impl_parse_iso!(LoraDate);
impl_parse_iso!(LoraTime);
impl_parse_iso!(LoraLocalTime);
impl_parse_iso!(LoraDateTime);
impl_parse_iso!(LoraLocalDateTime);
impl_parse_iso!(LoraDuration);
fn parse_iso<T: ParseIso>(obj: &serde_json::Map<String, J>, tag: &str) -> Result<T, String> {
let iso = obj
.get("iso")
.and_then(|v| v.as_str())
.ok_or_else(|| format!("{tag} value requires iso: string"))?;
T::parse_iso(iso)
}
fn point_from_obj(obj: &serde_json::Map<String, J>) -> Result<LoraPoint, String> {
let srid = obj.get("srid").and_then(|v| v.as_u64()).unwrap_or(7203) as u32;
let x = obj
.get("x")
.and_then(|v| v.as_f64())
.ok_or_else(|| "point.x must be a number".to_string())?;
let y = obj
.get("y")
.and_then(|v| v.as_f64())
.ok_or_else(|| "point.y must be a number".to_string())?;
let z = obj.get("z").and_then(|v| v.as_f64());
Ok(LoraPoint { x, y, z, srid })
}
fn vector_from_obj(obj: &serde_json::Map<String, J>) -> Result<LoraVector, String> {
let dimension = obj
.get("dimension")
.and_then(|v| v.as_i64())
.ok_or_else(|| "vector.dimension must be an integer".to_string())?;
let coordinate_type_name = obj
.get("coordinateType")
.and_then(|v| v.as_str())
.ok_or_else(|| "vector.coordinateType must be a string".to_string())?;
let coordinate_type = VectorCoordinateType::parse(coordinate_type_name)
.ok_or_else(|| format!("unknown vector coordinate type `{coordinate_type_name}`"))?;
let values = obj
.get("values")
.and_then(|v| v.as_array())
.ok_or_else(|| "vector.values must be an array of numbers".to_string())?;
let mut raw = Vec::with_capacity(values.len());
for v in values {
match v {
J::Number(n) => {
if let Some(i) = n.as_i64() {
raw.push(RawCoordinate::Int(i));
} else if let Some(f) = n.as_f64() {
raw.push(RawCoordinate::Float(f));
} else {
return Err("vector.values entries must be finite numbers".into());
}
}
_ => return Err("vector.values entries must be numbers".into()),
}
}
LoraVector::try_new(raw, dimension, coordinate_type).map_err(|e| e.to_string())
}
fn binary_from_obj(obj: &serde_json::Map<String, J>) -> Result<LoraBinary, String> {
let segments = obj
.get("segments")
.and_then(|v| v.as_array())
.ok_or_else(|| "binary.segments must be an array of byte arrays".to_string())?;
let mut out = Vec::with_capacity(segments.len());
for segment in segments {
let values = segment
.as_array()
.ok_or_else(|| "binary segment must be an array of bytes".to_string())?;
let mut chunk = Vec::with_capacity(values.len());
for value in values {
let byte = value
.as_u64()
.ok_or_else(|| "binary byte must be an integer 0..255".to_string())?;
chunk.push(
u8::try_from(byte).map_err(|_| "binary byte must be in 0..=255".to_string())?,
);
}
out.push(chunk);
}
Ok(LoraBinary::from_segments(out))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scalars_round_trip() {
for v in [
LoraValue::Null,
LoraValue::Bool(true),
LoraValue::Int(42),
LoraValue::Float(2.5),
LoraValue::String("hi".into()),
] {
let j = lora_value_to_json(&v);
assert_eq!(lora_value_from_json(j).unwrap(), v);
}
}
#[test]
fn nested_map_round_trips() {
let mut m = BTreeMap::new();
m.insert("name".to_string(), LoraValue::String("alice".into()));
m.insert(
"tags".to_string(),
LoraValue::List(vec![LoraValue::String("a".into()), LoraValue::Int(2)]),
);
let v = LoraValue::Map(m);
let j = lora_value_to_json(&v);
assert_eq!(lora_value_from_json(j).unwrap(), v);
}
}