plugx_input/
diff.rs

1use crate::{position::InputPosition, Input};
2use serde::Serialize;
3use std::fmt::{Display, Formatter};
4
5#[derive(Debug, Clone, Serialize)]
6pub struct InputDiff {
7    input: Input,
8    position: InputPosition,
9    maybe_old_value: Option<Input>,
10    maybe_new_value: Option<Input>,
11    action: InputDiffAction,
12}
13
14#[derive(Debug, Clone, PartialEq, Serialize)]
15pub enum InputDiffAction {
16    Added,
17    Removed,
18    Updated(Option<String>),
19}
20
21impl InputDiff {
22    pub fn input(&self) -> &Input {
23        &self.input
24    }
25
26    pub fn position(&self) -> &InputPosition {
27        &self.position
28    }
29
30    pub fn maybe_old_value(&self) -> Option<&Input> {
31        self.maybe_old_value.as_ref()
32    }
33
34    pub fn maybe_new_value(&self) -> Option<&Input> {
35        self.maybe_new_value.as_ref()
36    }
37
38    pub fn action(&self) -> &InputDiffAction {
39        &self.action
40    }
41}
42
43impl Display for InputDiff {
44    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
45        let position = if self.position.is_empty() {
46            "".to_string()
47        } else {
48            format!("{} ", self.position)
49        };
50        let description = &self.action;
51        let text = match (self.maybe_old_value.as_ref(), self.maybe_new_value.as_ref()) {
52            (Some(old_value), Some(new_value)) => {
53                format!("{position}value `{old_value}` {description} to new value `{new_value}`")
54            }
55            (Some(old_value), None) => {
56                format!("{position}value `{old_value}` {description}")
57            }
58            (None, Some(new_value)) => {
59                format!("{position}value `{new_value}` {description}")
60            }
61            (None, None) => format!("{position}hasn't changed"),
62        };
63        f.write_str(text.as_str())
64    }
65}
66
67impl Display for InputDiffAction {
68    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
69        f.write_str(match self {
70            Self::Added => "added",
71            Self::Removed => "removed",
72            Self::Updated(None) => "updated",
73            Self::Updated(Some(updated)) => updated,
74        })
75    }
76}
77
78pub fn diff<F>(input_1: &Input, input_2: &Input, for_each_function: &mut F)
79where
80    F: FnMut(InputDiff),
81{
82    diff_with_position(input_1, input_2, for_each_function, InputPosition::new());
83}
84
85pub fn diff_with_position<F>(
86    input_1: &Input,
87    input_2: &Input,
88    for_each_function: &mut F,
89    position: InputPosition,
90) where
91    F: FnMut(InputDiff),
92{
93    if input_1 == input_2 {
94        return;
95    }
96    if input_1.is_map() && input_2.is_map() {
97        let (old_map, new_map) = (input_1.as_map(), input_2.as_map());
98        for (key, old_value) in old_map {
99            let new_position = position.new_with_key(key);
100            if let Some(new_value) = new_map.get(key) {
101                diff_with_position(old_value, new_value, for_each_function, new_position);
102            } else {
103                let diff = InputDiff {
104                    input: input_1.clone(),
105                    position: new_position,
106                    maybe_old_value: Some(old_value.clone()),
107                    maybe_new_value: None,
108                    action: InputDiffAction::Removed,
109                };
110                for_each_function(diff);
111            }
112        }
113        for (key, new_value) in new_map {
114            let new_position = position.new_with_key(key);
115            if !old_map.contains_key(key) {
116                let diff = InputDiff {
117                    input: input_1.clone(),
118                    position: new_position,
119                    maybe_old_value: None,
120                    maybe_new_value: Some(new_value.clone()),
121                    action: InputDiffAction::Added,
122                };
123                for_each_function(diff);
124            }
125        }
126    } else if input_1.is_list() && input_2.is_list() {
127        let (old_list, new_list) = (input_1.as_list(), input_2.as_list());
128        let (mut added_index_list, mut removed_index_list) = (Vec::new(), Vec::new());
129        for (old_index, inner_input) in old_list.iter().enumerate() {
130            if !new_list.contains(inner_input) {
131                removed_index_list.push(old_index)
132            }
133        }
134        for (new_index, inner_input) in new_list.iter().enumerate() {
135            if !old_list.contains(inner_input) {
136                added_index_list.push(new_index)
137            }
138        }
139        for added_index in &added_index_list {
140            let added_index = *added_index;
141            let new_position = position.new_with_index(added_index);
142            if removed_index_list.contains(&added_index) {
143                diff_with_position(
144                    old_list.get(added_index).unwrap(),
145                    new_list.get(added_index).unwrap(),
146                    for_each_function,
147                    new_position,
148                );
149            } else {
150                let diff = InputDiff {
151                    input: input_1.clone(),
152                    position: new_position,
153                    maybe_old_value: None,
154                    maybe_new_value: Some(new_list.get(added_index).unwrap().clone()),
155                    action: InputDiffAction::Added,
156                };
157                for_each_function(diff);
158            }
159        }
160        for removed_index in removed_index_list {
161            let new_position = position.new_with_index(removed_index);
162            if !added_index_list.contains(&removed_index) {
163                let diff = InputDiff {
164                    input: input_1.clone(),
165                    position: new_position,
166                    maybe_old_value: Some(old_list.get(removed_index).unwrap().clone()),
167                    maybe_new_value: None,
168                    action: InputDiffAction::Removed,
169                };
170                for_each_function(diff);
171            }
172        }
173    } else if input_1.is_str() && input_2.is_str() {
174        let diff = InputDiff {
175            input: input_1.clone(),
176            position,
177            maybe_old_value: Some(input_1.clone()),
178            maybe_new_value: Some(input_2.clone()),
179            action: InputDiffAction::Updated(None),
180        };
181        for_each_function(diff);
182    } else if input_1.is_int() && input_2.is_int() {
183        let (old_int, new_int) = (*input_1.as_int(), *input_2.as_int());
184        let description = if old_int < new_int {
185            format!("increased by {}", new_int - old_int)
186        } else {
187            format!("decreased by {}", old_int - new_int)
188        };
189        let diff = InputDiff {
190            input: input_1.clone(),
191            position,
192            maybe_old_value: Some(input_1.clone()),
193            maybe_new_value: Some(input_2.clone()),
194            action: InputDiffAction::Updated(Some(description)),
195        };
196        for_each_function(diff);
197    } else if input_1.is_float() && input_2.is_float() {
198        let (old_float, new_float) = (*input_1.as_float(), *input_2.as_float());
199        let description = if old_float < new_float {
200            format!("increased by {}", new_float - old_float)
201        } else {
202            format!("decreased by {}", old_float - new_float)
203        };
204        let diff = InputDiff {
205            input: input_1.clone(),
206            position,
207            maybe_old_value: Some(input_1.clone()),
208            maybe_new_value: Some(input_2.clone()),
209            action: InputDiffAction::Updated(Some(description)),
210        };
211        for_each_function(diff);
212    } else if input_1.is_bool() && input_2.is_bool() {
213        let diff = InputDiff {
214            input: input_1.clone(),
215            position,
216            maybe_old_value: Some(input_1.clone()),
217            maybe_new_value: Some(input_2.clone()),
218            action: InputDiffAction::Updated(None),
219        };
220        for_each_function(diff);
221    } else {
222        // Changed to different type:
223        let diff = InputDiff {
224            input: input_1.clone(),
225            position,
226            maybe_old_value: Some(input_1.clone()),
227            maybe_new_value: Some(input_2.clone()),
228            action: InputDiffAction::Updated(Some(format!(
229                "changed from {} type to {} type",
230                input_1.type_name(),
231                input_2.type_name()
232            ))),
233        };
234        for_each_function(diff);
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::logging::{enable_logging, info};
242    use crate::position::InputPositionType;
243    use std::collections::HashMap;
244
245    fn diff_to_list(old_input: &Input, new_input: &Input, print: bool) -> Vec<InputDiff> {
246        enable_logging();
247
248        let mut diff_list = Vec::new();
249        diff(old_input, new_input, &mut |diff| {
250            if print {
251                info(format!("\t{diff}"));
252            }
253            diff_list.push(diff)
254        });
255        diff_list
256    }
257
258    #[test]
259    fn functionality() {
260        enable_logging();
261
262        let old_input = Input::from(true);
263        let mut new_input = Input::from(true);
264        let diff_list = diff_to_list(&old_input, &new_input, false);
265        assert!(diff_list.is_empty());
266        *new_input.bool_mut() = false;
267        let diff_list = diff_to_list(&old_input, &new_input, false);
268        assert!(!diff_list.is_empty());
269
270        let old_input = Input::from(100);
271        let mut new_input = Input::from(100);
272        let diff_list = diff_to_list(&old_input, &new_input, false);
273        assert!(diff_list.is_empty());
274        *new_input.int_mut() = -100;
275        let diff_list = diff_to_list(&old_input, &new_input, false);
276        assert!(!diff_list.is_empty());
277        assert_eq!(
278            diff_list[0].action.to_string(),
279            "decreased by 200".to_string()
280        );
281
282        let old_input = Input::from(-1.5);
283        let mut new_input = Input::from(-1.5);
284        let diff_list = diff_to_list(&old_input, &new_input, false);
285        assert!(diff_list.is_empty());
286        *new_input.float_mut() = 3.0;
287        let diff_list = diff_to_list(&old_input, &new_input, false);
288        assert!(!diff_list.is_empty());
289        assert_eq!(
290            diff_list[0].action.to_string(),
291            "increased by 4.5".to_string()
292        );
293
294        let old_input = Input::from("foo");
295        let mut new_input = Input::from("foo");
296        let diff_list = diff_to_list(&old_input, &new_input, false);
297        assert!(diff_list.is_empty());
298        *new_input.str_mut() = "bar".to_string();
299        let diff_list = diff_to_list(&old_input, &new_input, false);
300        assert!(!diff_list.is_empty());
301        assert_eq!(diff_list[0].action, InputDiffAction::Updated(None));
302
303        let old_input = Input::from(Vec::from([
304            Input::from(true),
305            Input::from(10),
306            Input::from(0.5),
307            Input::from("foo"),
308        ]));
309        let mut new_input = Input::from(Vec::from([
310            Input::from(true),
311            Input::from(10),
312            Input::from(0.5),
313            Input::from("foo"),
314        ]));
315        let diff_list = diff_to_list(&old_input, &new_input, false);
316        assert!(diff_list.is_empty());
317        *new_input.list_mut()[0].bool_mut() = false;
318        let diff_list = diff_to_list(&old_input, &new_input, false);
319        assert_eq!(diff_list.len(), 1);
320        assert_eq!(
321            diff_list[0].position.clone()[0],
322            InputPositionType::Index(0)
323        );
324        *new_input.list_mut()[1].int_mut() = 20;
325        let diff_list = diff_to_list(&old_input, &new_input, false);
326        assert_eq!(diff_list.len(), 2);
327        assert_eq!(
328            diff_list[1].position.clone()[0],
329            InputPositionType::Index(1)
330        );
331        *new_input.list_mut()[2].float_mut() = -0.5;
332        let diff_list = diff_to_list(&old_input, &new_input, false);
333        assert_eq!(diff_list.len(), 3);
334        assert_eq!(
335            diff_list[2].position.clone()[0],
336            InputPositionType::Index(2)
337        );
338        *new_input.list_mut()[3].str_mut() = "bar".to_string();
339        let diff_list = diff_to_list(&old_input, &new_input, false);
340        assert_eq!(diff_list.len(), 4);
341        assert_eq!(
342            diff_list[3].position.clone()[0],
343            InputPositionType::Index(3)
344        );
345
346        let old_input = Input::from(HashMap::from([(
347            "foo".to_string(),
348            Input::from(Vec::from([
349                Input::from(true),
350                Input::from(10),
351                Input::from(0.5),
352                Input::from("foo"),
353            ])),
354        )]));
355        let mut new_input = Input::from(HashMap::from([(
356            "foo".to_string(),
357            Input::from(Vec::from([
358                Input::from(true),
359                Input::from(10),
360                Input::from(0.5),
361                Input::from("foo"),
362            ])),
363        )]));
364        let diff_list = diff_to_list(&old_input, &new_input, false);
365        assert!(diff_list.is_empty());
366        *new_input.map_mut().get_mut("foo").unwrap().list_mut()[1].int_mut() = 100;
367        let diff_list = diff_to_list(&old_input, &new_input, false);
368        assert!(!diff_list.is_empty());
369        assert_eq!(
370            diff_list[0].position.clone()[0],
371            InputPositionType::Key("foo".to_string())
372        );
373        assert_eq!(
374            diff_list[0].position.clone()[1],
375            InputPositionType::Index(1)
376        );
377    }
378
379    #[test]
380    fn print() {
381        enable_logging();
382
383        let json = serde_json::json!({"foo": {"bar": {"baz": [-10, 1.5, false, "bar"]}}});
384        let input: Input = serde_json::from_value(json.clone()).unwrap();
385        let mut new_input = input.clone();
386        let list = new_input
387            .map_mut()
388            .get_mut("foo")
389            .unwrap()
390            .map_mut()
391            .get_mut("bar")
392            .unwrap()
393            .map_mut()
394            .get_mut("baz")
395            .unwrap()
396            .list_mut();
397        *list[0].int_mut() = 10;
398        *list[1].float_mut() = -1.5;
399        list[2] = Input::from("new string");
400        list.remove(3);
401        info(format!("Original data: {}", json.to_string()));
402        info(format!(
403            "Updated data:  {}",
404            serde_json::to_string(&new_input).unwrap()
405        ));
406        info(format!("Updates:"));
407        diff_to_list(&input, &new_input, true);
408    }
409}