Skip to main content

serde_patch/
diff_patch.rs

1use serde_json::{Map, Value};
2use std::collections::HashSet;
3
4/// Recursively computes a JSON diff between two values (internal).
5///
6/// Returns a partial JSON value containing only changed fields (new values)
7/// and optionally forced fields (even if unchanged).
8///
9/// If no differences (and no forced fields apply), returns `Value::Null` or an empty object.
10pub fn compute_diff(
11    old: Option<&Value>,
12    new: &Value,
13    forced: &HashSet<String>,
14    current_path: &str,
15) -> Option<Value> {
16    if let (Some(old_obj), Value::Object(new_map)) = (old.and_then(|v| v.as_object()), new) {
17        let old_map = old_obj;
18        let mut diff_map: Map<String, Value> = Map::new();
19
20        for (key, new_value) in new_map {
21            let full_path = if current_path.is_empty() {
22                key.clone()
23            } else {
24                format!("{}.{}", current_path, key)
25            };
26
27            let old_value = old_map.get(key);
28
29            if let Some(diff_value) = compute_diff(old_value, new_value, forced, &full_path) {
30                diff_map.insert(key.clone(), diff_value);
31            } else if forced.contains(&full_path) {
32                diff_map.insert(key.clone(), new_value.clone());
33            }
34        }
35
36        for key in old_map.keys() {
37            if !new_map.contains_key(key) {
38                diff_map.insert(key.clone(), Value::Null);
39            }
40        }
41
42        if diff_map.is_empty() {
43            None
44        } else {
45            Some(Value::Object(diff_map))
46        }
47    } else {
48        let equal = old == Some(new);
49        if equal && !forced.contains(current_path) {
50            None
51        } else {
52            Some(new.clone())
53        }
54    }
55}
56
57/// Computes a JSON diff suitable for use as a Merge Patch (RFC 7396).
58///
59/// Returns a `serde_json::Value` containing only changed fields (with new values).
60/// If no changes, returns an empty object.
61///
62/// See also [`diff_including`] for a version that can force inclusion of specific fields.
63///
64/// # Example
65///
66/// ```
67/// use serde_json::json;
68///
69/// #[derive(serde::Serialize)]
70/// struct User { id: u32, name: String, age: u8 }
71///
72/// let old = User { id: 1, name: "old".to_string(), age: 31 };
73/// let new = User { id: 1, name: "new".to_string(), age: 31 };
74///
75/// let patch = serde_patch::diff(&old, &new).unwrap();
76/// assert_eq!(patch, json!({ "name": "new" }));
77/// ```
78pub fn diff<T: serde::Serialize>(old: &T, new: &T) -> Result<serde_json::Value, serde_json::Error> {
79    let old_val = serde_json::to_value(old)?;
80    let new_val = serde_json::to_value(new)?;
81    let diff_opt = compute_diff(Some(&old_val), &new_val, &HashSet::new(), "");
82    Ok(diff_opt.unwrap_or(serde_json::Value::Object(serde_json::Map::new())))
83}
84
85/// Computes a JSON diff, forcing specific fields to be included even if unchanged.
86///
87/// This is useful when you need to provide context (like an ID) in the patch,
88/// regardless of whether that field has changed.
89///
90/// # Example
91///
92/// ```
93/// use serde_json::json;
94///
95/// #[derive(serde::Serialize)]
96/// struct User { id: u32, name: String }
97///
98/// let old = User { id: 1, name: "old".to_string() };
99/// let new = User { id: 1, name: "new".to_string() };
100///
101/// // "id" is included even though it didn't change
102/// let patch = serde_patch::diff_including(&old, &new, &["id"]).unwrap();
103/// assert_eq!(patch, json!({ "id": 1, "name": "new" }));
104/// ```
105pub fn diff_including<T: serde::Serialize>(
106    old: &T,
107    new: &T,
108    including: &[&str],
109) -> Result<serde_json::Value, serde_json::Error> {
110    let old_val = serde_json::to_value(old)?;
111    let new_val = serde_json::to_value(new)?;
112    let including_set: HashSet<String> = including.iter().map(|s| s.to_string()).collect();
113    let diff_opt = compute_diff(Some(&old_val), &new_val, &including_set, "");
114    Ok(diff_opt.unwrap_or(serde_json::Value::Object(serde_json::Map::new())))
115}