Skip to main content

elasticctl_api/
report.rs

1use serde::ser::SerializeStruct;
2use serde::{Serialize, Serializer};
3use serde_json::Value;
4
5#[derive(Debug, Clone)]
6pub struct ReportEntry {
7    pub rule_id: String,
8    pub name: String,
9    pub action: String,
10    pub before: Option<Value>,
11    pub after: Option<Value>,
12    pub applied: bool,
13    pub error: Option<String>,
14    pub value_list: Option<crate::value_lists::Evidence>,
15}
16
17impl Serialize for ReportEntry {
18    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
19        if let Some(evidence) = &self.value_list {
20            return evidence.serialize(serializer);
21        }
22        let mut value = serializer.serialize_struct("ReportEntry", 7)?;
23        value.serialize_field("rule_id", &self.rule_id)?;
24        value.serialize_field("name", &self.name)?;
25        value.serialize_field("action", &self.action)?;
26        value.serialize_field("before", &self.before)?;
27        value.serialize_field("after", &self.after)?;
28        value.serialize_field("applied", &self.applied)?;
29        value.serialize_field("error", &self.error)?;
30        value.end()
31    }
32}
33
34impl From<crate::value_lists::Evidence> for ReportEntry {
35    fn from(evidence: crate::value_lists::Evidence) -> Self {
36        Self {
37            rule_id: evidence.rule_id.clone(),
38            name: evidence.name.clone(),
39            action: serde_json::to_value(evidence.action)
40                .expect("action serializes")
41                .as_str()
42                .unwrap()
43                .into(),
44            before: None,
45            after: None,
46            applied: evidence.outcome == crate::value_lists::Outcome::Applied,
47            error: None,
48            value_list: Some(evidence),
49        }
50    }
51}
52
53#[derive(Debug, Clone, Serialize)]
54pub struct ChangeReport {
55    pub profile: String,
56    pub host: String,
57    pub space: String,
58    pub applied: bool,
59    pub entries: Vec<ReportEntry>,
60}
61
62impl ChangeReport {
63    pub fn counts(&self) -> (usize, usize, usize, usize) {
64        let created = self
65            .entries
66            .iter()
67            .filter(|e| e.action == "create" && e.applied)
68            .count();
69        let updated = self
70            .entries
71            .iter()
72            .filter(|e| e.action == "update" && e.applied)
73            .count();
74        let skipped = self
75            .entries
76            .iter()
77            .filter(|e| e.action == "skipped_remote_only")
78            .count();
79        let failed = self.entries.iter().filter(|e| e.error.is_some()).count();
80        (created, updated, skipped, failed)
81    }
82
83    pub fn pending(&self) -> usize {
84        self.entries
85            .iter()
86            .filter(|e| {
87                matches!(e.action.as_str(), "create" | "update") && !e.applied && e.error.is_none()
88            })
89            .count()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    fn entry(action: &str, applied: bool, error: Option<&str>) -> ReportEntry {
98        ReportEntry {
99            rule_id: "x".into(),
100            name: "X".into(),
101            action: action.into(),
102            before: None,
103            after: None,
104            applied,
105            error: error.map(String::from),
106            value_list: None,
107        }
108    }
109
110    fn report(entries: Vec<ReportEntry>) -> ChangeReport {
111        ChangeReport {
112            profile: "default".into(),
113            host: "kb.example.com".into(),
114            space: "default".into(),
115            applied: false,
116            entries,
117        }
118    }
119
120    #[test]
121    fn pending_counts_unapplied_unfailed_create_and_update_entries() {
122        let r = report(vec![
123            entry("create", false, None),
124            entry("update", false, None),
125            entry("skipped_remote_only", false, None),
126        ]);
127        assert_eq!(r.pending(), 2);
128    }
129
130    #[test]
131    fn pending_is_zero_once_every_actionable_entry_has_an_outcome() {
132        let r = report(vec![
133            entry("create", true, None),
134            entry("update", false, Some("conflict")),
135        ]);
136        assert_eq!(r.pending(), 0, "applied or failed entries are not pending");
137    }
138
139    #[test]
140    fn counts_are_unaffected_by_pending_entries() {
141        let r = report(vec![
142            entry("create", false, None),
143            entry("update", true, None),
144        ]);
145        let (created, updated, skipped, failed) = r.counts();
146        assert_eq!((created, updated, skipped, failed), (0, 1, 0, 0));
147    }
148}