rustic_core/repofile/snapshotfile/
grouping.rs1use std::{
2 cmp::Ordering,
3 fmt::{self, Display},
4 str::FromStr,
5};
6
7use derive_setters::Setters;
8use itertools::Itertools;
9use serde::{Deserialize, Serialize};
10use serde_with::skip_serializing_none;
11
12use crate::{
13 ForgetSnapshot, StringList,
14 repofile::{
15 SnapshotFile,
16 snapshotfile::{SnapshotFileErrorKind, SnapshotFileResult},
17 },
18};
19
20#[allow(clippy::struct_excessive_bools)]
24#[derive(Clone, Debug, Copy, Setters, Deserialize, Serialize)]
25#[setters(into)]
26#[non_exhaustive]
27pub struct SnapshotGroupCriterion {
28 pub hostname: bool,
30
31 pub label: bool,
33
34 pub paths: bool,
36
37 pub tags: bool,
39}
40
41impl SnapshotGroupCriterion {
42 #[must_use]
44 pub fn new() -> Self {
45 Self {
46 hostname: false,
47 label: false,
48 paths: false,
49 tags: false,
50 }
51 }
52
53 #[must_use]
55 pub fn from_group(group: &SnapshotGroup) -> Self {
56 Self {
57 hostname: group.hostname.is_some(),
58 label: group.label.is_some(),
59 paths: group.paths.is_some(),
60 tags: group.tags.is_some(),
61 }
62 }
63}
64
65impl Default for SnapshotGroupCriterion {
66 fn default() -> Self {
67 Self {
68 hostname: true,
69 label: true,
70 paths: true,
71 tags: false,
72 }
73 }
74}
75
76impl FromStr for SnapshotGroupCriterion {
77 type Err = SnapshotFileErrorKind;
78 fn from_str(s: &str) -> SnapshotFileResult<Self> {
79 let mut crit = Self::new();
80 for val in s.split(',') {
81 match val {
82 "host" => crit.hostname = true,
83 "label" => crit.label = true,
84 "paths" => crit.paths = true,
85 "tags" => crit.tags = true,
86 "" => {}
87 v => return Err(SnapshotFileErrorKind::ValueNotAllowed(v.into())),
88 }
89 }
90 Ok(crit)
91 }
92}
93
94impl Display for SnapshotGroupCriterion {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 let mut display = Vec::new();
97 if self.hostname {
98 display.push("host");
99 }
100 if self.label {
101 display.push("label");
102 }
103 if self.paths {
104 display.push("paths");
105 }
106 if self.tags {
107 display.push("tags");
108 }
109 write!(f, "{}", display.join(","))?;
110 Ok(())
111 }
112}
113
114#[skip_serializing_none]
115#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
116#[non_exhaustive]
117pub struct SnapshotGroup {
119 pub hostname: Option<String>,
121
122 pub label: Option<String>,
124
125 pub paths: Option<StringList>,
127
128 pub tags: Option<StringList>,
130}
131
132impl PartialOrd for SnapshotGroup {
133 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
134 Some(self.cmp(other))
135 }
136}
137
138impl Ord for SnapshotGroup {
139 fn cmp(&self, other: &Self) -> Ordering {
140 self.hostname
141 .cmp(&other.hostname)
142 .then(self.label.cmp(&other.label))
143 .then(self.paths.cmp(&other.paths))
144 .then(self.tags.cmp(&other.tags))
145 }
146}
147
148impl Display for SnapshotGroup {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 let mut out = Vec::new();
151
152 if let Some(host) = &self.hostname {
153 out.push(format!("host [{host}]"));
154 }
155 if let Some(label) = &self.label {
156 out.push(format!("label [{label}]"));
157 }
158 if let Some(paths) = &self.paths {
159 out.push(format!("paths [{paths}]"));
160 }
161 if let Some(tags) = &self.tags {
162 out.push(format!("tags [{tags}]"));
163 }
164
165 write!(f, "({})", out.join(", "))?;
166 Ok(())
167 }
168}
169
170impl SnapshotGroup {
171 #[must_use]
178 pub fn from_snapshot(sn: &SnapshotFile, crit: SnapshotGroupCriterion) -> Self {
179 Self {
180 hostname: crit.hostname.then(|| sn.hostname.clone()),
181 label: crit.label.then(|| sn.label.clone()),
182 paths: crit.paths.then(|| sn.paths.clone()),
183 tags: crit.tags.then(|| sn.tags.clone()),
184 }
185 }
186
187 #[must_use]
193 pub fn matches(&self, snapshot: &SnapshotFile) -> bool {
194 self.hostname
195 .as_ref()
196 .is_none_or(|val| val == &snapshot.hostname)
197 && self.label.as_ref().is_none_or(|val| val == &snapshot.label)
198 && self.paths.as_ref().is_none_or(|val| val == &snapshot.paths)
199 && self.tags.as_ref().is_none_or(|val| val == &snapshot.tags)
200 }
201
202 #[must_use]
204 pub fn is_empty(&self) -> bool {
205 self == &Self::default()
206 }
207}
208
209pub trait Grouping {
210 type GroupKey: PartialEq + Ord + fmt::Debug;
211 type Criterion: Copy;
212 fn get_group(&self, c: Self::Criterion) -> Self::GroupKey;
213}
214
215impl Grouping for SnapshotFile {
216 type GroupKey = SnapshotGroup;
217 type Criterion = SnapshotGroupCriterion;
218 fn get_group(&self, c: Self::Criterion) -> Self::GroupKey {
219 SnapshotGroup::from_snapshot(self, c)
220 }
221}
222
223impl Grouping for ForgetSnapshot {
224 type GroupKey = SnapshotGroup;
225 type Criterion = SnapshotGroupCriterion;
226 fn get_group(&self, c: Self::Criterion) -> Self::GroupKey {
227 SnapshotGroup::from_snapshot(&self.snapshot, c)
228 }
229}
230
231#[derive(Debug, Clone, Deserialize, Serialize)]
232pub struct Group<T: Grouping> {
234 pub group_key: T::GroupKey,
236 pub items: Vec<T>,
238}
239
240impl<T: Grouping> Group<T>
241where
242 T::GroupKey: Default,
243{
244 #[must_use]
246 pub fn default_group(items: Vec<T>) -> Self {
247 Self {
248 group_key: T::GroupKey::default(),
249 items,
250 }
251 }
252}
253
254#[derive(Debug)]
255pub struct Grouped<T: Grouping> {
257 pub criterion: T::Criterion,
259 pub groups: Vec<Group<T>>,
261}
262
263impl<T: Grouping> Grouped<T> {
264 #[must_use]
266 pub fn new(criterion: T::Criterion) -> Self {
267 Self {
268 criterion,
269 groups: Vec::new(),
270 }
271 }
272
273 #[must_use]
275 pub fn from_items(mut items: Vec<T>, criterion: T::Criterion) -> Self {
276 items.sort_unstable_by_key(|item| item.get_group(criterion));
277 let mut groups = Vec::new();
278 for (group, snaps) in &items.into_iter().chunk_by(|item| item.get_group(criterion)) {
279 groups.push(Group {
280 group_key: group,
281 items: snaps.collect(),
282 });
283 }
284 Self { criterion, groups }
285 }
286
287 pub fn try_update_with<E>(
293 self,
294 update: impl FnOnce(Vec<T>) -> Result<Vec<T>, E>,
295 ) -> Result<Self, E> {
296 let crit = self.criterion;
297 let items = update(self.into())?;
298 Ok(Self::from_items(items, crit))
299 }
300}
301
302impl<T: Grouping> From<Grouped<T>> for Vec<T> {
303 fn from(value: Grouped<T>) -> Self {
304 value
305 .groups
306 .into_iter()
307 .flat_map(|group| group.items)
308 .collect()
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use rstest::rstest;
316
317 #[rstest]
318 #[case(
319 "host,label,paths",
320 true,
321 true,
322 true,
323 false,
324 "host,label,paths",
325 "(host [myhost], label [mylabel], paths [/path])"
326 )]
327 #[case("host", true, false, false, false, "host", "(host [myhost])")]
328 #[case(
329 "label,host",
330 true,
331 true,
332 false,
333 false,
334 "host,label",
335 "(host [myhost], label [mylabel])"
336 )]
337 #[case("tags", false, false, false, true, "tags", "(tags [tag1,tag2])")]
338 #[case(
339 "paths,label",
340 false,
341 true,
342 true,
343 false,
344 "label,paths",
345 "(label [mylabel], paths [/path])"
346 )]
347 fn fromstr_display(
348 #[case] input: String,
349 #[case] is_host: bool,
350 #[case] is_label: bool,
351 #[case] is_path: bool,
352 #[case] is_tags: bool,
353 #[case] display: String,
354 #[case] group_display: String,
355 ) {
356 let crit: SnapshotGroupCriterion = input.parse().unwrap();
357 assert_eq!(crit.hostname, is_host);
358 assert_eq!(crit.label, is_label);
359 assert_eq!(crit.paths, is_path);
360 assert_eq!(crit.tags, is_tags);
361
362 assert_eq!(crit.to_string(), display);
363
364 let sn = SnapshotFile {
365 hostname: "myhost".to_string(),
366 label: "mylabel".to_string(),
367 paths: "/path".parse().unwrap(),
368 tags: "tag1,tag2".parse().unwrap(),
369 ..Default::default()
370 };
371
372 let group = SnapshotGroup::from_snapshot(&sn, crit);
373 assert_eq!(group.to_string(), group_display);
374 }
375}