merge_struct/
lib.rs

1#![warn(missing_docs)]
2//!
3//! `merge-struct` is a simple library for deep merging two serializable structs.
4//!
5//!
6//! ## Examples
7//! Deserialize two structs and merge them
8//!
9//! ```no_run
10//! use std::collections::BTreeMap;
11//! use serde_json;
12//! use serde::{Deserialize, Serialize};
13//! use merge_struct::merge;
14//!
15//! #[derive(Serialize, Deserialize)]
16//! struct Data {
17//!   is_root: Option<bool>,
18//!   folders: Vec<Folder>,
19//!   entries: Option<BTreeMap<String, Entry>>, // btree so test results will be ordered and stable between runs
20//! }
21//! #[derive(Serialize, Deserialize)]
22//! struct Folder {
23//!     name: String,
24//!     num_files: Option<u32>,
25//! }
26//! #[derive(Serialize, Deserialize)]
27//! struct Entry {
28//!     name: String,
29//!     size: u32,
30//! }
31//!
32//! let left: Data = serde_json::from_str(
33//!     r###"
34//! {
35//!     "is_root": false,
36//!     "entries": {
37//!         "/var/log/f2": {
38//!             "name":"f2",
39//!             "size": 5
40//!         }
41//!     },
42//!     "folders": [
43//!         {
44//!             "name": "/var/log",
45//!             "num_files": 20
46//!         }
47//!     ]
48//! }
49//! "###,
50//! )
51//! .unwrap();
52//! let right: Data = serde_json::from_str(
53//!     r###"
54//! {
55//!     "folders":[],
56//!     "entries": {
57//!         "/var/log/f1": {
58//!             "name":"f1",
59//!             "size": 12
60//!         }
61//!     }
62//! }
63//! "###,
64//! )
65//! .unwrap();
66//! let res = merge(&left, &right);
67//!```
68//!
69
70use serde_json::Error;
71use serde_json::Value;
72
73fn to_value<T: serde::ser::Serialize>(value: &T) -> Result<serde_json::Value, Error> {
74    serde_json::to_value(value)
75}
76
77fn from_value<T: serde::ser::Serialize + serde::de::DeserializeOwned>(
78    value: serde_json::Value,
79) -> Result<T, Error> {
80    serde_json::from_value(value)
81}
82
83fn merge_value(a: &mut Value, b: &Value) {
84    match (a, b) {
85        (Value::Object(ref mut a), &Value::Object(ref b)) => {
86            for (k, v) in b {
87                merge_value(a.entry(k).or_insert(Value::Null), v);
88            }
89        }
90        (Value::Array(ref mut a), &Value::Array(ref b)) => {
91            a.extend(b.clone());
92        }
93        (Value::Array(ref mut a), &Value::Object(ref b)) => {
94            a.extend([Value::Object(b.clone())]);
95        }
96        (_, Value::Null) => {} // do nothing
97        (a, b) => {
98            *a = b.clone();
99        }
100    }
101}
102
103///
104/// deep merge two structs that are serializable.
105/// based on turning them into json::Value and merging that.
106///
107/// # Errors
108/// Will return an error if serialization fails
109///
110pub fn merge<T: serde::ser::Serialize + serde::de::DeserializeOwned>(
111    base: &T,
112    overrides: &T,
113) -> Result<T, Error> {
114    let mut left = to_value(base)?;
115    let right = to_value(overrides)?;
116    merge_value(&mut left, &right);
117    from_value(left)
118}
119
120#[cfg(test)]
121mod tests {
122    use std::collections::BTreeMap;
123
124    use serde::{Deserialize, Serialize};
125
126    use super::*;
127    use insta::assert_yaml_snapshot;
128
129    #[derive(Serialize, Deserialize)]
130    struct Data {
131        is_root: Option<bool>,
132        folders: Vec<Folder>,
133        entries: Option<BTreeMap<String, Entry>>, // btree so test results will be ordered and stable between runs
134    }
135
136    #[derive(Serialize, Deserialize)]
137    struct Folder {
138        name: String,
139        num_files: Option<u32>,
140    }
141
142    #[derive(Serialize, Deserialize)]
143    struct Entry {
144        name: String,
145        size: u32,
146    }
147    #[test]
148    fn test_merge_left_empty() {
149        let left: Data = serde_json::from_str(
150            r###"
151        {
152            "is_root": false,
153            "folders": []
154        }
155        "###,
156        )
157        .unwrap();
158        let right: Data = serde_json::from_str(
159            r###"
160        {
161            "is_root": true,
162            "folders":[
163                {
164                    "name": "/var/log",
165                    "num_files": 20
166                }
167            ],
168            "entries": {
169                "/var/log/f1": {
170                    "name":"f1",
171                    "size": 12
172                }
173            }
174        }
175        "###,
176        )
177        .unwrap();
178        assert_yaml_snapshot!(merge(&left, &right).unwrap());
179    }
180    #[test]
181    fn test_merge_right_empty() {
182        let right: Data = serde_json::from_str(
183            r###"
184        {
185            "is_root": false,
186            "folders": []
187        }
188        "###,
189        )
190        .unwrap();
191        let left: Data = serde_json::from_str(
192            r###"
193        {
194            "is_root": true,
195            "folders":[
196                {
197                    "name": "/var/log",
198                    "num_files": 20
199                }
200            ],
201            "entries": {
202                "/var/log/f1": {
203                    "name":"f1",
204                    "size": 12
205                }
206            }
207        }
208        "###,
209        )
210        .unwrap();
211        assert_yaml_snapshot!(merge(&left, &right).unwrap());
212    }
213
214    #[test]
215    fn test_merge() {
216        let left: Data = serde_json::from_str(
217            r###"
218        {
219            "is_root": false,
220            "entries": {
221                "/var/log/f2": {
222                    "name":"f2",
223                    "size": 5
224                }
225            },
226            "folders": [
227                {
228                    "name": "/var/log",
229                    "num_files": 20
230                }
231            ]
232        }
233        "###,
234        )
235        .unwrap();
236        let right: Data = serde_json::from_str(
237            r###"
238        {
239            "folders":[],
240            "entries": {
241                "/var/log/f1": {
242                    "name":"f1",
243                    "size": 12
244                }
245            }
246        }
247        "###,
248        )
249        .unwrap();
250        assert_yaml_snapshot!(merge(&left, &right).unwrap());
251    }
252}