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