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}