diffo 0.2.0

Semantic diffing for Rust structs via serde
Documentation
use crate::{Change, Diff, Error, Path};
use serde::{Deserialize, Serialize};
use serde_value::Value;
use std::collections::BTreeMap;

/// Apply a diff to a base value, producing a new value with changes applied.
///
/// # Examples
///
/// ```
/// use diffo::{diff, apply};
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
/// struct User {
///     name: String,
///     age: u32,
/// }
///
/// let old = User { name: "Alice".into(), age: 30 };
/// let new = User { name: "Alice".into(), age: 31 };
///
/// let d = diff(&old, &new).unwrap();
/// let result: User = apply(&old, &d).unwrap();
///
/// assert_eq!(result, new);
/// ```
pub fn apply<T>(base: &T, diff: &Diff) -> Result<T, Error>
where
    T: Serialize + for<'de> Deserialize<'de>,
{
    let mut value = serde_value::to_value(base)?;

    let mut changes: Vec<_> = diff.changes().iter().collect();

    changes.sort_by(|(path_a, change_a), (path_b, change_b)| {
        use std::cmp::Ordering;

        match (change_a, change_b) {
            (Change::Removed(_), Change::Removed(_)) => {
                match (path_a.last_index(), path_b.last_index()) {
                    (Some(idx_a), Some(idx_b)) => idx_b.cmp(&idx_a),
                    _ => path_b.as_str().cmp(path_a.as_str()),
                }
            }
            (Change::Removed(_), _) => Ordering::Greater,
            (_, Change::Removed(_)) => Ordering::Less,
            _ => path_a.as_str().cmp(path_b.as_str()),
        }
    });

    for (path, change) in changes {
        apply_change(&mut value, path, change)?;
    }

    Ok(T::deserialize(value)?)
}

/// Apply a single change to a Value at the given path.
fn apply_change(root: &mut Value, path: &Path, change: &Change) -> Result<(), Error> {
    match change {
        Change::Added(new_value) => {
            set_at_path(root, path, new_value.clone())?;
        }
        Change::Removed(_) => {
            remove_at_path(root, path)?;
        }
        Change::Modified { to, .. } => {
            set_at_path(root, path, to.clone())?;
        }
        Change::Elided { .. } => {
            return Err(Error::Config(format!(
                "Cannot apply elided change at path: {}",
                path.as_str()
            )));
        }
    }

    Ok(())
}

/// Set a value at the given path in the Value tree.
fn set_at_path(root: &mut Value, path: &Path, new_value: Value) -> Result<(), Error> {
    let path_str = path.as_str();

    if path_str.is_empty() {
        *root = new_value;
        return Ok(());
    }

    let segments = parse_path(path_str);

    let (parent_segments, last_segment) = segments.split_at(segments.len() - 1);
    let parent = navigate_to_mut(root, parent_segments)?;

    match last_segment[0] {
        PathSegment::Field(ref field_name) => {
            if let Value::Map(ref mut map) = parent {
                let key = Value::String(field_name.clone());
                map.insert(key, new_value);
                Ok(())
            } else {
                Err(Error::InvalidPath(format!(
                    "Cannot set field '{}' on non-map value",
                    field_name
                )))
            }
        }
        PathSegment::Index(idx) => {
            if let Value::Seq(ref mut seq) = parent {
                if idx < seq.len() {
                    seq[idx] = new_value;
                    Ok(())
                } else if idx == seq.len() {
                    seq.push(new_value);
                    Ok(())
                } else {
                    Err(Error::InvalidPath(format!(
                        "Index {} out of bounds for sequence of length {}",
                        idx,
                        seq.len()
                    )))
                }
            } else {
                Err(Error::InvalidPath(format!(
                    "Cannot index non-sequence value at index {}",
                    idx
                )))
            }
        }
    }
}

/// Remove a value at the given path.
fn remove_at_path(root: &mut Value, path: &Path) -> Result<(), Error> {
    let path_str = path.as_str();

    if path_str.is_empty() {
        return Err(Error::InvalidPath("Cannot remove root value".to_string()));
    }

    let segments = parse_path(path_str);
    let (parent_segments, last_segment) = segments.split_at(segments.len() - 1);
    let parent = navigate_to_mut(root, parent_segments)?;

    match last_segment[0] {
        PathSegment::Field(ref field_name) => {
            if let Value::Map(ref mut map) = parent {
                let key = Value::String(field_name.clone());
                map.remove(&key);
                Ok(())
            } else {
                Err(Error::InvalidPath(format!(
                    "Cannot remove field '{}' from non-map value",
                    field_name
                )))
            }
        }
        PathSegment::Index(idx) => {
            if let Value::Seq(ref mut seq) = parent {
                if idx < seq.len() {
                    seq.remove(idx);
                    Ok(())
                } else {
                    Err(Error::InvalidPath(format!(
                        "Index {} out of bounds for sequence of length {}",
                        idx,
                        seq.len()
                    )))
                }
            } else {
                Err(Error::InvalidPath(format!(
                    "Cannot remove index {} from non-sequence value",
                    idx
                )))
            }
        }
    }
}

/// Navigate to a mutable reference at the given path segments.
fn navigate_to_mut<'a>(
    mut current: &'a mut Value,
    segments: &[PathSegment],
) -> Result<&'a mut Value, Error> {
    for segment in segments {
        current = match segment {
            PathSegment::Field(field_name) => {
                if let Value::Map(ref mut map) = current {
                    let key = Value::String(field_name.clone());
                    map.entry(key).or_insert(Value::Map(BTreeMap::new()))
                } else {
                    return Err(Error::InvalidPath(format!(
                        "Cannot navigate through field '{}' on non-map value",
                        field_name
                    )));
                }
            }
            PathSegment::Index(idx) => {
                if let Value::Seq(ref mut seq) = current {
                    if *idx < seq.len() {
                        &mut seq[*idx]
                    } else {
                        return Err(Error::InvalidPath(format!(
                            "Index {} out of bounds for sequence of length {}",
                            idx,
                            seq.len()
                        )));
                    }
                } else {
                    return Err(Error::InvalidPath(format!(
                        "Cannot navigate through index {} on non-sequence value",
                        idx
                    )));
                }
            }
        };
    }

    Ok(current)
}

/// Path segment (field or array index).
#[derive(Debug, Clone, PartialEq)]
enum PathSegment {
    Field(String),
    Index(usize),
}

/// Parse a path string into segments.
fn parse_path(path: &str) -> Vec<PathSegment> {
    let mut segments = Vec::new();
    let mut current = String::new();
    let mut chars = path.chars().peekable();

    while let Some(ch) = chars.next() {
        match ch {
            '.' => {
                if !current.is_empty() {
                    segments.push(PathSegment::Field(current.clone()));
                    current.clear();
                }
            }
            '[' => {
                if !current.is_empty() {
                    segments.push(PathSegment::Field(current.clone()));
                    current.clear();
                }

                let mut index_str = String::new();
                while let Some(&next_ch) = chars.peek() {
                    if next_ch == ']' {
                        chars.next();
                        break;
                    }
                    index_str.push(chars.next().unwrap());
                }

                if let Ok(idx) = index_str.parse::<usize>() {
                    segments.push(PathSegment::Index(idx));
                }
            }
            _ => {
                current.push(ch);
            }
        }
    }

    if !current.is_empty() {
        segments.push(PathSegment::Field(current));
    }

    segments
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_path_simple_field() {
        let segments = parse_path("name");
        assert_eq!(segments, vec![PathSegment::Field("name".into())]);
    }

    #[test]
    fn test_parse_path_nested_field() {
        let segments = parse_path("user.name");
        assert_eq!(
            segments,
            vec![
                PathSegment::Field("user".into()),
                PathSegment::Field("name".into())
            ]
        );
    }

    #[test]
    fn test_parse_path_array_index() {
        let segments = parse_path("[0]");
        assert_eq!(segments, vec![PathSegment::Index(0)]);
    }

    #[test]
    fn test_parse_path_field_and_index() {
        let segments = parse_path("users[0].name");
        assert_eq!(
            segments,
            vec![
                PathSegment::Field("users".into()),
                PathSegment::Index(0),
                PathSegment::Field("name".into())
            ]
        );
    }

    #[test]
    fn test_parse_path_multiple_indices() {
        let segments = parse_path("data[0][1]");
        assert_eq!(
            segments,
            vec![
                PathSegment::Field("data".into()),
                PathSegment::Index(0),
                PathSegment::Index(1)
            ]
        );
    }
}