1use serde::Serialize;
6use serde_json::Value;
7
8#[derive(Debug, Clone, Serialize)]
9pub struct ReportEntry {
10 pub rule_id: String,
11 pub name: String,
12 pub action: String,
14 pub before: Option<Value>,
15 pub after: Option<Value>,
16 pub applied: bool,
17 pub error: Option<String>,
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub struct ChangeReport {
22 pub profile: String,
23 pub host: String,
24 pub space: String,
25 pub applied: bool,
27 pub entries: Vec<ReportEntry>,
28}
29
30impl ChangeReport {
31 pub fn counts(&self) -> (usize, usize, usize, usize) {
32 let created = self
33 .entries
34 .iter()
35 .filter(|e| e.action == "create" && e.applied)
36 .count();
37 let updated = self
38 .entries
39 .iter()
40 .filter(|e| e.action == "update" && e.applied)
41 .count();
42 let skipped = self
43 .entries
44 .iter()
45 .filter(|e| e.action == "skipped_remote_only")
46 .count();
47 let failed = self.entries.iter().filter(|e| e.error.is_some()).count();
48 (created, updated, skipped, failed)
49 }
50
51 pub fn pending(&self) -> usize {
56 self.entries
57 .iter()
58 .filter(|e| {
59 matches!(e.action.as_str(), "create" | "update") && !e.applied && e.error.is_none()
60 })
61 .count()
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 fn entry(action: &str, applied: bool, error: Option<&str>) -> ReportEntry {
70 ReportEntry {
71 rule_id: "x".into(),
72 name: "X".into(),
73 action: action.into(),
74 before: None,
75 after: None,
76 applied,
77 error: error.map(String::from),
78 }
79 }
80
81 fn report(entries: Vec<ReportEntry>) -> ChangeReport {
82 ChangeReport {
83 profile: "default".into(),
84 host: "kb.example.com".into(),
85 space: "default".into(),
86 applied: false,
87 entries,
88 }
89 }
90
91 #[test]
92 fn pending_counts_unapplied_unfailed_create_and_update_entries() {
93 let r = report(vec![
94 entry("create", false, None),
95 entry("update", false, None),
96 entry("skipped_remote_only", false, None),
97 ]);
98 assert_eq!(r.pending(), 2);
99 }
100
101 #[test]
102 fn pending_is_zero_once_every_actionable_entry_has_an_outcome() {
103 let r = report(vec![
104 entry("create", true, None),
105 entry("update", false, Some("conflict")),
106 ]);
107 assert_eq!(r.pending(), 0, "applied or failed entries are not pending");
108 }
109
110 #[test]
111 fn counts_are_unaffected_by_pending_entries() {
112 let r = report(vec![
113 entry("create", false, None),
114 entry("update", true, None),
115 ]);
116 let (created, updated, skipped, failed) = r.counts();
117 assert_eq!((created, updated, skipped, failed), (0, 1, 0, 0));
118 }
119}