weavegraph 0.7.0

Graph-driven, concurrent agent workflow framework with versioned state, deterministic barrier merges, and rich diagnostics.
Documentation
//! JSON manipulation utilities and extensions for the Weavegraph framework.
//!
//! Provides deep-merge, dot-path access, and common JSON manipulation patterns.

use serde_json::{Map, Value};
use std::collections::HashMap;
use thiserror::Error;

/// Errors that can occur during JSON operations.
#[derive(Debug, Error)]
#[cfg_attr(feature = "diagnostics", derive(miette::Diagnostic))]
pub enum JsonError {
    /// Invalid JSON pointer format.
    #[error("Invalid JSON pointer: {pointer}")]
    #[cfg_attr(
        feature = "diagnostics",
        diagnostic(code(weavegraph::json::invalid_pointer))
    )]
    InvalidPointer {
        /// The offending pointer string.
        pointer: String,
    },

    /// Merge conflict that cannot be resolved.
    #[error("Merge conflict at path '{path}': cannot merge {left_type} with {right_type}")]
    #[cfg_attr(
        feature = "diagnostics",
        diagnostic(code(weavegraph::json::merge_conflict))
    )]
    MergeConflict {
        /// Dot-separated path where the conflict occurred.
        path: String,
        /// JSON type of the left operand.
        left_type: String,
        /// JSON type of the right operand.
        right_type: String,
    },

    /// Serialization or deserialization error.
    #[error("JSON serialization error: {source}")]
    #[cfg_attr(feature = "diagnostics", diagnostic(code(weavegraph::json::serde)))]
    Serde {
        /// Underlying serde_json error.
        #[from]
        source: serde_json::Error,
    },
}

/// Strategy for resolving conflicts during JSON merges.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeStrategy {
    /// Prefer the left value on conflict.
    PreferLeft,
    /// Prefer the right value on conflict.
    PreferRight,
    /// Fail on any conflict.
    FailOnConflict,
    /// Merge objects recursively; prefer right for primitive conflicts; concatenate arrays.
    DeepMerge,
}

/// Deep-merge two JSON values according to `strategy`.
///
/// Objects are always merged key-by-key. Arrays and scalars follow `strategy`.
///
/// ```rust
/// use weavegraph::utils::json_ext::{deep_merge, MergeStrategy};
/// use serde_json::json;
///
/// let left = json!({"a": 1, "b": {"x": 10}});
/// let right = json!({"b": {"y": 20}, "c": 3});
/// let merged = deep_merge(&left, &right, MergeStrategy::DeepMerge).unwrap();
/// assert_eq!(merged, json!({"a": 1, "b": {"x": 10, "y": 20}, "c": 3}));
/// ```
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",
    }
}

/// Fold-merge an iterator of JSON values using `strategy`.
///
/// ```rust
/// use weavegraph::utils::json_ext::{merge_multiple, MergeStrategy};
/// use serde_json::json;
///
/// let values = [json!({"a": 1}), json!({"b": 2}), json!({"c": 3})];
/// let merged = merge_multiple(values.iter(), MergeStrategy::DeepMerge).unwrap();
/// assert_eq!(merged, json!({"a": 1, "b": 2, "c": 3}));
/// ```
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)
}

/// Walk a dot-separated path into a JSON value.
///
/// Array segments are parsed as numeric indices.
///
/// ```rust
/// use weavegraph::utils::json_ext::get_by_path;
/// use serde_json::json;
///
/// let data = json!({"user": {"profile": {"name": "Alice"}}});
/// assert_eq!(get_by_path(&data, "user.profile.name"), Some(&json!("Alice")));
/// ```
#[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,
    })
}

/// Walk a dot-separated path and assign a value, auto-vivifying intermediate objects.
///
/// ```rust
/// use weavegraph::utils::json_ext::set_by_path;
/// use serde_json::{json, Value};
///
/// let mut data = json!({});
/// set_by_path(&mut data, "user.profile.name", json!("Alice")).unwrap();
/// assert_eq!(data, json!({"user": {"profile": {"name": "Alice"}}}));
/// ```
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(),
        }),
    }
}

/// Returns `true` if `value` is an object containing every key in `expected_keys`.
///
/// ```rust
/// use weavegraph::utils::json_ext::has_structure;
/// use serde_json::json;
///
/// let data = json!({"name": "Alice", "age": 30, "email": "alice@example.com"});
/// assert!(has_structure(&data, &["name", "email"]));
/// assert!(!has_structure(&data, &["name", "phone"]));
/// ```
#[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,
    }
}

/// Convert a `HashMap<String, V>` into a JSON object.
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())
}

/// Extension trait for `Value` with path-navigation and introspection helpers.
pub trait JsonValueExt {
    /// Return the value at `path`, or `default` if the path is absent.
    fn get_path_or<'a>(&'a self, path: &str, default: &'a Value) -> &'a Value;

    /// Return `true` if this is an empty object or array.
    fn is_empty_container(&self) -> bool;

    /// Number of elements for objects/arrays, or `1` for scalars.
    fn element_count(&self) -> usize;

    /// Object keys, or an empty vec for non-objects.
    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![],
        }
    }
}

/// Generic serialization interface for types that round-trip through JSON.
///
/// Generic over `E` so each module can map errors to its own type.
pub trait JsonSerializable<E>: serde::Serialize + for<'de> serde::de::DeserializeOwned {
    /// Serialize to a JSON string.
    fn to_json_string(&self) -> Result<String, E>;

    /// Deserialize from a JSON string.
    fn from_json_str(s: &str) -> Result<Self, E>;
}

/// Serialize `value` to a JSON string, mapping any error through `error_mapper`.
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))
}

/// Deserialize a JSON string, mapping any error through `error_mapper`.
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))
}