use serde_json::{Map, Value};
use std::collections::HashMap;
use thiserror::Error;
#[derive(Debug, Error)]
#[cfg_attr(feature = "diagnostics", derive(miette::Diagnostic))]
pub enum JsonError {
#[error("Invalid JSON pointer: {pointer}")]
#[cfg_attr(
feature = "diagnostics",
diagnostic(code(weavegraph::json::invalid_pointer))
)]
InvalidPointer {
pointer: String,
},
#[error("Merge conflict at path '{path}': cannot merge {left_type} with {right_type}")]
#[cfg_attr(
feature = "diagnostics",
diagnostic(code(weavegraph::json::merge_conflict))
)]
MergeConflict {
path: String,
left_type: String,
right_type: String,
},
#[error("JSON serialization error: {source}")]
#[cfg_attr(feature = "diagnostics", diagnostic(code(weavegraph::json::serde)))]
Serde {
#[from]
source: serde_json::Error,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeStrategy {
PreferLeft,
PreferRight,
FailOnConflict,
DeepMerge,
}
pub fn deep_merge(
left: &Value,
right: &Value,
strategy: MergeStrategy,
) -> Result<Value, JsonError> {
merge_at(left, right, strategy, "")
}
fn merge_at(
left: &Value,
right: &Value,
strategy: MergeStrategy,
path: &str,
) -> Result<Value, JsonError> {
match (left, right) {
(Value::Object(l), Value::Object(r)) => {
let mut result = Map::new();
for (key, lv) in l {
let child_path = if path.is_empty() {
key.clone()
} else {
format!("{path}.{key}")
};
let merged = match r.get(key) {
Some(rv) => merge_at(lv, rv, strategy, &child_path)?,
None => lv.clone(),
};
result.insert(key.clone(), merged);
}
for (key, rv) in r {
if !l.contains_key(key) {
result.insert(key.clone(), rv.clone());
}
}
Ok(Value::Object(result))
}
(Value::Array(l), Value::Array(r)) => match strategy {
MergeStrategy::PreferLeft => Ok(Value::Array(l.clone())),
MergeStrategy::PreferRight => Ok(Value::Array(r.clone())),
MergeStrategy::FailOnConflict => Err(JsonError::MergeConflict {
path: path.to_owned(),
left_type: "array".to_owned(),
right_type: "array".to_owned(),
}),
MergeStrategy::DeepMerge => {
let mut out = l.clone();
out.extend_from_slice(r);
Ok(Value::Array(out))
}
},
(lv, rv) if lv == rv => Ok(lv.clone()),
(lv, rv) => match strategy {
MergeStrategy::PreferLeft => Ok(lv.clone()),
MergeStrategy::PreferRight => Ok(rv.clone()),
MergeStrategy::FailOnConflict => Err(JsonError::MergeConflict {
path: path.to_owned(),
left_type: value_type(lv).to_owned(),
right_type: value_type(rv).to_owned(),
}),
MergeStrategy::DeepMerge => Ok(rv.clone()),
},
}
}
fn value_type(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
pub fn merge_multiple<'a, I>(values: I, strategy: MergeStrategy) -> Result<Value, JsonError>
where
I: IntoIterator<Item = &'a Value>,
{
let mut acc = Value::Object(Map::new());
for v in values {
acc = deep_merge(&acc, v, strategy)?;
}
Ok(acc)
}
#[must_use]
pub fn get_by_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
if path.is_empty() {
return Some(value);
}
path.split('.').try_fold(value, |cur, seg| match cur {
Value::Object(obj) => obj.get(seg),
Value::Array(arr) => arr.get(seg.parse::<usize>().ok()?),
_ => None,
})
}
pub fn set_by_path(target: &mut Value, path: &str, value: Value) -> Result<(), JsonError> {
if path.is_empty() {
*target = value;
return Ok(());
}
let mut segs: Vec<&str> = path.split('.').collect();
let final_key = segs.pop().expect("split yields at least one element");
let mut cur = target;
for &seg in &segs {
match cur {
Value::Object(obj) => {
cur = obj
.entry(seg.to_owned())
.or_insert_with(|| Value::Object(Map::new()));
}
_ => {
return Err(JsonError::InvalidPointer {
pointer: path.to_owned(),
});
}
}
}
match cur {
Value::Object(obj) => {
obj.insert(final_key.to_owned(), value);
Ok(())
}
_ => Err(JsonError::InvalidPointer {
pointer: path.to_owned(),
}),
}
}
#[must_use]
pub fn has_structure(value: &Value, expected_keys: &[&str]) -> bool {
match value {
Value::Object(obj) => expected_keys.iter().all(|k| obj.contains_key(*k)),
_ => false,
}
}
pub fn hashmap_to_json<V: Into<Value>>(map: HashMap<String, V>) -> Value {
Value::Object(map.into_iter().map(|(k, v)| (k, v.into())).collect())
}
pub trait JsonValueExt {
fn get_path_or<'a>(&'a self, path: &str, default: &'a Value) -> &'a Value;
fn is_empty_container(&self) -> bool;
fn element_count(&self) -> usize;
fn keys(&self) -> Vec<String>;
}
impl JsonValueExt for Value {
fn get_path_or<'a>(&'a self, path: &str, default: &'a Value) -> &'a Value {
get_by_path(self, path).unwrap_or(default)
}
fn is_empty_container(&self) -> bool {
match self {
Value::Object(obj) => obj.is_empty(),
Value::Array(arr) => arr.is_empty(),
_ => false,
}
}
fn element_count(&self) -> usize {
match self {
Value::Object(obj) => obj.len(),
Value::Array(arr) => arr.len(),
_ => 1,
}
}
fn keys(&self) -> Vec<String> {
match self {
Value::Object(obj) => obj.keys().cloned().collect(),
_ => vec![],
}
}
}
pub trait JsonSerializable<E>: serde::Serialize + for<'de> serde::de::DeserializeOwned {
fn to_json_string(&self) -> Result<String, E>;
fn from_json_str(s: &str) -> Result<Self, E>;
}
pub fn serialize_with_context<T, E>(
value: &T,
context: &str,
error_mapper: impl FnOnce(serde_json::Error, &str) -> E,
) -> Result<String, E>
where
T: serde::Serialize,
{
serde_json::to_string(value).map_err(|e| error_mapper(e, context))
}
pub fn deserialize_with_context<T, E>(
json: &str,
context: &str,
error_mapper: impl FnOnce(serde_json::Error, &str) -> E,
) -> Result<T, E>
where
T: serde::de::DeserializeOwned,
{
serde_json::from_str(json).map_err(|e| error_mapper(e, context))
}