yabe/
diff.rs

1use std::borrow::Cow;
2use std::collections::{HashMap, HashSet};
3
4use log::debug;
5use yaml_rust2::yaml::{Hash, Yaml};
6use crate::deep_equal::deep_equal;
7
8/// Recursively computes the difference between an override YAML object and the helm values YAML object.
9pub fn compute_diff<'a>(obj: &'a Yaml, helm: &'a Yaml) -> Option<Cow<'a, Yaml>> {
10    if deep_equal(obj, helm) {
11        None
12    } else {
13        match (obj, helm) {
14            (Yaml::Hash(obj_hash), Yaml::Hash(helm_hash)) => {
15                let mut diff_hash = Hash::new();
16                for (key, obj_value) in obj_hash {
17                    let helm_value = helm_hash.get(key).unwrap_or(&Yaml::Null);
18                    if let Some(diff_value) = compute_diff(obj_value, helm_value) {
19                        diff_hash.insert(key.clone(), diff_value.into_owned());
20                    }
21                }
22                if diff_hash.is_empty() {
23                    None
24                } else {
25                    Some(Cow::Owned(Yaml::Hash(diff_hash)))
26                }
27            }
28            (Yaml::Array(obj_array), Yaml::Array(helm_array)) => {
29                if obj_array.len() != helm_array.len() {
30                    Some(Cow::Borrowed(obj))
31                } else {
32                    let mut has_diff = false;
33                    let diffs: Vec<_> = obj_array
34                        .iter()
35                        .zip(helm_array.iter())
36                        .map(|(obj_item, helm_item)| {
37                            if let Some(diff_item) = compute_diff(obj_item, helm_item) {
38                                has_diff = true;
39                                diff_item.into_owned()
40                            } else {
41                                Yaml::Null
42                            }
43                        })
44                        .collect();
45
46                    if has_diff {
47                        Some(Cow::Owned(Yaml::Array(diffs)))
48                    } else {
49                        None
50                    }
51                }
52            }
53            _ => Some(Cow::Borrowed(obj)),
54        }
55    }
56}
57
58/// Recursively computes the common base and differences among multiple Yaml objects.
59pub fn diff_and_common_multiple<'a>(
60    objs: &'a [&'a Yaml],
61    quorum: f64,
62) -> (Option<Cow<'a, Yaml>>, Vec<Option<Cow<'a, Yaml>>>) {
63    debug!(
64        "diff_and_common_multiple called with {} objects and quorum {}%.",
65        objs.len(),
66        quorum * 100.0
67    );
68
69    if objs.is_empty() {
70        debug!("No objects to process. Returning.");
71        return (None, vec![]);
72    }
73
74    let total_files = objs.len();
75    let quorum_count = (quorum * total_files as f64).ceil() as usize;
76
77    // Collect types of each object and check for type differences in a single pass
78    let mut type_set = HashSet::new();
79    let mut obj_type = "";
80
81    for obj in objs {
82        let obj_type_str = match obj {
83            Yaml::Null => "null",
84            Yaml::Boolean(_) => "bool",
85            Yaml::Integer(_) => "int",
86            Yaml::Real(_) => "real",
87            Yaml::String(_) => "string",
88            Yaml::Array(_) => "array",
89            Yaml::Hash(_) => "hash",
90            _ => "unknown",
91        };
92        type_set.insert(obj_type_str);
93        if obj_type.is_empty() {
94            obj_type = obj_type_str;
95        }
96    }
97
98    // If types differ, include them in diffs
99    if type_set.len() > 1 {
100        debug!("Types differ. Including entire values in diffs.");
101        return (
102            None,
103            objs.iter().map(|obj| Some(Cow::Borrowed(*obj))).collect(),
104        );
105    }
106
107    // Handle primitive types and arrays as atomic units
108    if obj_type != "hash" {
109        debug!("Handling primitive types or arrays as atomic units.");
110
111        // Collect occurrences of unique values using deep comparison
112        let mut occurrences: HashMap<&Yaml, usize> = HashMap::new();
113        for obj in objs {
114            *occurrences.entry(*obj).or_insert(0) += 1;
115        }
116
117        // Find the value(s) that meet the quorum
118        let base_value = occurrences.iter().find_map(|(val, &count)| {
119            if count >= quorum_count {
120                Some(*val)
121            } else {
122                None
123            }
124        });
125
126        if let Some(base_val) = base_value {
127            debug!("Base value determined by quorum: {:?}", base_val);
128            let diffs = objs
129                .iter()
130                .map(|obj| {
131                    if deep_equal(obj, base_val) {
132                        None
133                    } else {
134                        Some(Cow::Borrowed(*obj))
135                    }
136                })
137                .collect();
138            return (Some(Cow::Borrowed(base_val)), diffs);
139        } else {
140            // No value meets the quorum; include all values in diffs
141            debug!("No value meets the quorum; including all values in diffs.");
142            return (
143                None,
144                objs.iter().map(|obj| Some(Cow::Borrowed(*obj))).collect(),
145            );
146        }
147    }
148
149    // Handle hashes (maps)
150    if obj_type == "hash" {
151        debug!("Handling hashes (maps).");
152        // Collect all unique keys
153        let mut all_keys = HashSet::new();
154        for obj in objs {
155            if let Yaml::Hash(ref h) = obj {
156                all_keys.extend(h.keys());
157            }
158        }
159
160        // Initialize base hash and diffs
161        let mut base_hash = Hash::new();
162        let mut diffs: Vec<Hash> = vec![Hash::new(); objs.len()];
163        let mut has_base = false;
164        let mut has_diffs = vec![false; objs.len()];
165
166        // Iterate over all keys
167        for key in &all_keys {
168            debug!("Processing key: {:?}", key);
169
170            // Collect values at current key from all objects
171            let values_at_key: Vec<&Yaml> = objs
172                .iter()
173                .map(|obj| {
174                    if let Yaml::Hash(ref h) = obj {
175                        h.get(*key).unwrap_or(&Yaml::Null)
176                    } else {
177                        &Yaml::Null
178                    }
179                })
180                .collect();
181
182            // Recursively process the values at this key
183            let (sub_base, sub_diffs) = diff_and_common_multiple(&values_at_key, quorum);
184
185            if let Some(ref sub_base_val) = sub_base {
186                // Base value meets quorum
187                base_hash.insert((*key).clone(), sub_base_val.clone().into_owned());
188                has_base = true;
189            }
190
191            let base_includes_key = sub_base.is_some();
192
193            for (i, sub_diff) in sub_diffs.into_iter().enumerate() {
194                if let Some(sub_diff_val) = sub_diff {
195                    if !sub_diff_val.is_null() || base_includes_key {
196                        diffs[i].insert((*key).clone(), sub_diff_val.into_owned());
197                        has_diffs[i] = true;
198                    }
199                }
200            }
201        }
202
203        // Prepare base and diffs for return
204        let base = if has_base {
205            Some(Cow::Owned(Yaml::Hash(base_hash)))
206        } else {
207            None
208        };
209        let diffs_result: Vec<Option<Cow<'a, Yaml>>> = diffs
210            .into_iter()
211            .enumerate()
212            .map(|(i, h)| {
213                if has_diffs[i] {
214                    Some(Cow::Owned(Yaml::Hash(h)))
215                } else {
216                    None
217                }
218            })
219            .collect();
220
221        return (base, diffs_result);
222    }
223
224    // Should not reach here; treat as diffs
225    debug!("Unhandled object type. Including entire values in diffs.");
226    (
227        None,
228        objs.iter().map(|obj| Some(Cow::Borrowed(*obj))).collect(),
229    )
230}