Skip to main content

ephemeris_forge/
diff.rs

1use std::fmt;
2
3use facet::Facet;
4use facet_value::Value;
5
6use crate::{NameSet, Report};
7
8/// A field value kept in a form that renders on one line.
9#[derive(Clone, Debug, Default, Eq, PartialEq)]
10pub struct FieldValue(Value);
11
12impl FieldValue {
13    /// Capture a value. A value that cannot be captured is absent.
14    #[must_use]
15    pub fn of<'a, T: Facet<'a>>(value: &T) -> Self {
16        Self(facet_value::to_value(value).unwrap_or_default())
17    }
18
19    /// Capture a value by its text form.
20    #[must_use]
21    pub fn text<D: fmt::Display>(value: &D) -> Self {
22        Self(Value::from(value.to_string()))
23    }
24}
25
26impl fmt::Display for FieldValue {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        if self.0.is_null() {
29            return f.write_str("none");
30        }
31        if let Some(text) = self.0.as_string() {
32            return f.write_str(text.as_str());
33        }
34        match facet_json::to_string(&self.0) {
35            Ok(json) => f.write_str(&json),
36            Err(_) => f.write_str("?"),
37        }
38    }
39}
40
41/// One field that differs, with its live and desired values.
42#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct FieldChange {
44    /// The field name.
45    pub name: &'static str,
46    /// The live value.
47    pub old: FieldValue,
48    /// The desired value.
49    pub new: FieldValue,
50}
51
52impl fmt::Display for FieldChange {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(f, "{}: {} -> {}", self.name, self.old, self.new)
55    }
56}
57
58/// What the plan does to one resource.
59#[derive(Clone, Debug, Eq, PartialEq)]
60pub enum Change {
61    /// The resource does not exist yet.
62    Create,
63    /// The resource exists and is not declared.
64    Delete,
65    /// The resource exists with other values, in name order.
66    Update(Vec<FieldChange>),
67}
68
69impl Change {
70    /// The names of the changed fields.
71    #[must_use]
72    pub fn fields(&self) -> Vec<&'static str> {
73        match self {
74            Self::Create | Self::Delete => Vec::new(),
75            Self::Update(fields) => fields.iter().map(|field| field.name).collect(),
76        }
77    }
78
79    /// An update of a single field.
80    #[must_use]
81    pub fn field(name: &'static str, old: FieldValue, new: FieldValue) -> Self {
82        Self::Update(vec![FieldChange { name, old, new }])
83    }
84
85    /// The report lines for `resource`, one per changed field.
86    pub fn lines<'a>(&'a self, resource: &'a str) -> impl Iterator<Item = String> + 'a {
87        let lines: Vec<String> = match self {
88            Self::Create => vec![format!("{resource}: create")],
89            Self::Delete => vec![format!("{resource}: delete")],
90            Self::Update(fields) => fields
91                .iter()
92                .map(|field| format!("{resource}: {field}"))
93                .collect(),
94        };
95        lines.into_iter()
96    }
97}
98
99/// Fields compared on one resource, split into those that differ and those
100/// that already match.
101#[derive(Clone, Debug, Default)]
102pub struct FieldDiff {
103    changed: Vec<FieldChange>,
104    same: Vec<&'static str>,
105}
106
107impl FieldDiff {
108    /// Note one compared field and return whether it differs.
109    pub fn check<'a, O: Facet<'a>, N: Facet<'a>>(
110        &mut self,
111        name: &'static str,
112        equal: bool,
113        old: &O,
114        new: &N,
115    ) -> bool {
116        if equal {
117            self.same.push(name);
118        } else {
119            self.changed.push(FieldChange {
120                name,
121                old: FieldValue::of(old),
122                new: FieldValue::of(new),
123            });
124        }
125        !equal
126    }
127
128    /// Compare one field with its live value, if the resource exists.
129    pub fn compare<'a, T: Facet<'a> + PartialEq>(
130        &mut self,
131        name: &'static str,
132        old: Option<&'a T>,
133        new: &T,
134    ) -> bool {
135        self.check(name, old == Some(new), &old, new)
136    }
137
138    /// Compare a declared field with its live value.
139    ///
140    /// Returns the value to write when the field is declared and differs. An
141    /// undeclared field is not compared.
142    pub fn want<'a, T, D, L>(&mut self, name: &'static str, desired: D, live: L) -> Option<T>
143    where
144        T: Facet<'a> + Clone + PartialEq,
145        D: Into<Option<&'a T>>,
146        L: Into<Option<&'a T>>,
147    {
148        let wanted = desired.into()?;
149        let live = live.into();
150        self.check(name, live == Some(wanted), &live, wanted)
151            .then(|| wanted.clone())
152    }
153
154    /// Compare a declared set with its live value. A missing live value is
155    /// the empty set.
156    ///
157    /// Returns the set to write when it differs.
158    pub fn want_set(
159        &mut self,
160        name: &'static str,
161        desired: &NameSet,
162        live: Option<&NameSet>,
163    ) -> Option<NameSet> {
164        let empty = NameSet::default();
165        let live = live.unwrap_or(&empty);
166        self.check(name, live == desired, &live.as_slice(), &desired.as_slice())
167            .then(|| desired.clone())
168    }
169
170    /// Whether any compared field differs.
171    #[must_use]
172    pub const fn has_changes(&self) -> bool {
173        !self.changed.is_empty()
174    }
175
176    /// Add the compared fields to `report` under `resource`, in name order.
177    pub fn record(mut self, resource: &str, report: &mut Report) {
178        self.changed.sort_unstable_by_key(|field| field.name);
179        self.same.sort_unstable();
180        if !self.changed.is_empty() {
181            report
182                .changes
183                .insert(resource.to_owned(), Change::Update(self.changed));
184        }
185        if !self.same.is_empty() {
186            report.unchanged.insert(
187                resource.to_owned(),
188                self.same.into_iter().map(str::to_owned).collect(),
189            );
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn an_update_lists_each_changed_field_with_both_values() {
200        let mut diff = FieldDiff::default();
201        diff.check("enforcement", false, &"evaluate", &"active");
202        diff.check("target", true, &"branch", &"branch");
203        diff.check("bypass_actors", false, &Vec::<u32>::new(), &vec![1u32, 2]);
204        diff.check("homepage", false, &None::<String>, &Some("x".to_owned()));
205        let mut report = Report::default();
206        diff.record("ruleset main", &mut report);
207        assert_eq!(
208            report.lines().collect::<Vec<_>>(),
209            [
210                "ruleset main: bypass_actors: [] -> [1,2]",
211                "ruleset main: enforcement: evaluate -> active",
212                "ruleset main: homepage: none -> x",
213            ]
214        );
215        assert_eq!(report.unchanged["ruleset main"], ["target"]);
216    }
217
218    #[test]
219    fn want_returns_a_declared_value_that_differs() {
220        let mut diff = FieldDiff::default();
221        assert_eq!(diff.want("a", Some(&1), Some(&2)), Some(1));
222        assert_eq!(diff.want("b", Some(&1), Some(&1)), None);
223        assert_eq!(diff.want("c", None::<&i32>, Some(&1)), None);
224        assert_eq!(diff.want("d", Some(&1), None), Some(1));
225        let mut report = Report::default();
226        diff.record("thing", &mut report);
227        assert_eq!(report.unchanged["thing"], ["b"]);
228    }
229
230    #[test]
231    fn want_set_ignores_order_and_a_missing_live_set_is_empty() {
232        let mut diff = FieldDiff::default();
233        let wanted = NameSet::new(["b".to_owned(), "a".to_owned()]);
234        let live = NameSet::new(["a".to_owned(), "b".to_owned()]);
235        assert_eq!(diff.want_set("same", &wanted, Some(&live)), None);
236        assert_eq!(
237            diff.want_set("missing", &wanted, None).as_ref(),
238            Some(&wanted)
239        );
240        assert_eq!(diff.want_set("empty", &NameSet::default(), None), None);
241    }
242
243    #[test]
244    fn create_and_delete_print_one_word() {
245        let mut report = Report::default();
246        report.changes.insert("label a".to_owned(), Change::Create);
247        report.record_delete("label b".to_owned());
248        assert_eq!(
249            report.lines().collect::<Vec<_>>(),
250            ["label a: create", "label b: delete"]
251        );
252    }
253}