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> {
deep_merge_with_path(left, right, strategy, "")
}
fn deep_merge_with_path(
left: &Value,
right: &Value,
strategy: MergeStrategy,
path: &str,
) -> Result<Value, JsonError> {
match (left, right) {
(Value::Object(left_obj), Value::Object(right_obj)) => {
let mut result = Map::new();
for (key, value) in left_obj {
let current_path = if path.is_empty() {
key.clone()
} else {
format!("{}.{}", path, key)
};
if let Some(right_value) = right_obj.get(key) {
let merged = deep_merge_with_path(value, right_value, strategy, ¤t_path)?;
result.insert(key.clone(), merged);
} else {
result.insert(key.clone(), value.clone());
}
}
for (key, value) in right_obj {
if !left_obj.contains_key(key) {
result.insert(key.clone(), value.clone());
}
}
Ok(Value::Object(result))
}
(Value::Array(left_arr), Value::Array(right_arr)) => match strategy {
MergeStrategy::PreferLeft => Ok(Value::Array(left_arr.clone())),
MergeStrategy::PreferRight => Ok(Value::Array(right_arr.clone())),
MergeStrategy::FailOnConflict => Err(JsonError::MergeConflict {
path: path.to_string(),
left_type: "array".to_string(),
right_type: "array".to_string(),
}),
MergeStrategy::DeepMerge => {
let mut result = left_arr.clone();
result.extend(right_arr.clone());
Ok(Value::Array(result))
}
},
(left_val, right_val) if left_val == right_val => Ok(left_val.clone()),
(left_val, right_val) => match strategy {
MergeStrategy::PreferLeft => Ok(left_val.clone()),
MergeStrategy::PreferRight => Ok(right_val.clone()),
MergeStrategy::FailOnConflict => Err(JsonError::MergeConflict {
path: path.to_string(),
left_type: get_value_type(left_val).to_string(),
right_type: get_value_type(right_val).to_string(),
}),
MergeStrategy::DeepMerge => {
Ok(right_val.clone())
}
},
}
}
fn get_value_type(value: &Value) -> &'static str {
match value {
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 result = Value::Object(Map::new());
for value in values {
result = deep_merge(&result, value, strategy)?;
}
Ok(result)
}
#[must_use]
pub fn get_by_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
if path.is_empty() {
return Some(value);
}
let parts: Vec<&str> = path.split('.').collect();
let mut current = value;
for part in parts {
match current {
Value::Object(obj) => {
current = obj.get(part)?;
}
Value::Array(arr) => {
let index: usize = part.parse().ok()?;
current = arr.get(index)?;
}
_ => return None,
}
}
Some(current)
}
pub fn set_by_path(target: &mut Value, path: &str, value: Value) -> Result<(), JsonError> {
if path.is_empty() {
*target = value;
return Ok(());
}
let parts: Vec<&str> = path.split('.').collect();
let mut current = target;
for part in &parts[..parts.len() - 1] {
match current {
Value::Object(obj) => {
current = obj
.entry(part.to_string())
.or_insert_with(|| Value::Object(Map::new()));
}
_ => {
return Err(JsonError::InvalidPointer {
pointer: path.to_string(),
});
}
}
}
let final_key = parts[parts.len() - 1];
match current {
Value::Object(obj) => {
obj.insert(final_key.to_string(), value);
Ok(())
}
_ => Err(JsonError::InvalidPointer {
pointer: path.to_string(),
}),
}
}
#[must_use]
pub fn has_structure(value: &Value, expected_keys: &[&str]) -> bool {
match value {
Value::Object(obj) => expected_keys.iter().all(|key| obj.contains_key(*key)),
_ => false,
}
}
pub fn hashmap_to_json<V: Into<Value>>(map: HashMap<String, V>) -> Value {
let json_map: Map<String, Value> = map.into_iter().map(|(k, v)| (k, v.into())).collect();
Value::Object(json_map)
}
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>;
fn deep_clone(&self) -> Value;
}
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![],
}
}
fn deep_clone(&self) -> Value {
self.clone()
}
}
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))
}