1use std::collections::BTreeMap;
2
3use crate::{Change, FieldDiff, Mode, Report};
4
5#[derive(Clone, Copy, Debug)]
7pub struct Collection<'a> {
8 pub noun: &'a str,
10 pub mode: Mode,
12}
13
14impl Collection<'_> {
15 pub fn plan<K, D, L, O, M, F, R>(
27 self,
28 declared: &BTreeMap<K, D>,
29 live: &[L],
30 report: &mut Report,
31 matches: M,
32 mut diff: F,
33 mut retire: R,
34 ) -> Vec<O>
35 where
36 K: AsRef<str>,
37 M: Fn(&L, &str) -> bool,
38 F: FnMut(&str, &D, Option<&L>, &mut Report) -> (FieldDiff, Vec<O>),
39 R: FnMut(&L, &mut Report) -> Option<(String, O)>,
40 {
41 let mut operations = Vec::new();
42 for (key, desired) in declared {
43 let key = key.as_ref();
44 let actual = live.iter().find(|item| matches(item, key));
45 let (fields, changes) = diff(key, desired, actual, report);
46 let resource = format!("{} {key}", self.noun);
47 if actual.is_none() && fields.has_changes() {
48 report.changes.insert(resource, Change::Create);
49 } else {
50 fields.record(&resource, report);
51 }
52 operations.extend(changes);
53 }
54 if self.mode == Mode::Owned {
55 for item in live {
56 if declared.keys().any(|key| matches(item, key.as_ref())) {
57 continue;
58 }
59 if let Some((key, operation)) = retire(item, report) {
60 report.record_delete(format!("{} {key}", self.noun));
61 operations.push(operation);
62 }
63 }
64 }
65 operations
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[derive(Debug, Eq, PartialEq)]
74 enum Operation {
75 Create(String),
76 Patch(String),
77 Delete(String),
78 }
79
80 fn run(mode: Mode, declared: &[(&str, u32)], live: &[(&str, u32)]) -> (Vec<Operation>, Report) {
81 let declared: BTreeMap<String, u32> = declared
82 .iter()
83 .map(|(key, value)| ((*key).to_owned(), *value))
84 .collect();
85 let live: Vec<(String, u32)> = live
86 .iter()
87 .map(|(key, value)| ((*key).to_owned(), *value))
88 .collect();
89 let mut report = Report::default();
90 let operations = Collection { noun: "item", mode }.plan(
91 &declared,
92 &live,
93 &mut report,
94 |item, key| item.0 == key,
95 |key, desired, actual, _| {
96 let mut fields = FieldDiff::default();
97 let changed = fields.check(
98 "value",
99 actual.is_some_and(|item| item.1 == *desired),
100 &actual.map(|item| item.1),
101 desired,
102 );
103 let operations = match actual {
104 None => vec![Operation::Create(key.to_owned())],
105 Some(_) if changed => vec![Operation::Patch(key.to_owned())],
106 Some(_) => Vec::new(),
107 };
108 (fields, operations)
109 },
110 |item, _| Some((item.0.clone(), Operation::Delete(item.0.clone()))),
111 );
112 (operations, report)
113 }
114
115 #[test]
116 fn matching_item_yields_nothing() {
117 let (operations, report) = run(Mode::Owned, &[("a", 1)], &[("a", 1)]);
118 assert_eq!(operations, []);
119 assert!(report.changes.is_empty());
120 assert_eq!(report.unchanged["item a"], ["value"]);
121 }
122
123 #[test]
124 fn differing_item_yields_the_diff_operations() {
125 let (operations, report) = run(Mode::Additive, &[("a", 2)], &[("a", 1)]);
126 assert_eq!(operations, [Operation::Patch("a".to_owned())]);
127 assert_eq!(report.changes["item a"].fields(), ["value"]);
128 }
129
130 #[test]
131 fn missing_item_is_created() {
132 let (operations, report) = run(Mode::Additive, &[("a", 1)], &[]);
133 assert_eq!(operations, [Operation::Create("a".to_owned())]);
134 assert_eq!(report.changes["item a"], Change::Create);
135 }
136
137 #[test]
138 fn additive_mode_keeps_undeclared_items() {
139 let (operations, report) = run(Mode::Additive, &[("a", 1)], &[("a", 1), ("b", 1)]);
140 assert_eq!(operations, []);
141 assert!(!report.changes.contains_key("item b"));
142 }
143
144 #[test]
145 fn owned_mode_deletes_undeclared_items_after_declared_ones() {
146 let (operations, report) = run(
147 Mode::Owned,
148 &[("a", 2), ("c", 1)],
149 &[("z", 1), ("a", 1), ("b", 1)],
150 );
151 assert_eq!(
152 operations,
153 [
154 Operation::Patch("a".to_owned()),
155 Operation::Create("c".to_owned()),
156 Operation::Delete("z".to_owned()),
157 Operation::Delete("b".to_owned()),
158 ]
159 );
160 assert_eq!(report.changes["item z"], Change::Delete);
161 assert_eq!(report.changes["item b"], Change::Delete);
162 }
163}