Skip to main content

systemprompt_logging/services/cli/
summary.rs

1//! Aggregate result summaries for CLI output.
2//!
3//! [`ValidationSummary`] accumulates module-validation outcomes (valid,
4//! installed, updated, schemas/seeds applied, disabled) and renders them as a
5//! grouped report; [`OperationResult`] and [`ProgressSummary`] report single
6//! operations and batch progress through the [`Display`] trait.
7
8use std::io::Write;
9
10use crate::services::cli::display::{CollectionDisplay, Display, DisplayUtils, StatusDisplay};
11use crate::services::cli::theme::{EmphasisType, ItemStatus, MessageLevel, Theme};
12#[derive(Debug)]
13pub struct ValidationSummary {
14    pub valid: Vec<(String, String)>,
15    pub installed: Vec<String>,
16    pub updated: Vec<String>,
17    pub schemas_applied: Vec<String>,
18    pub seeds_applied: Vec<String>,
19    pub disabled: Vec<String>,
20}
21
22impl ValidationSummary {
23    pub const fn new() -> Self {
24        Self {
25            valid: Vec::new(),
26            installed: Vec::new(),
27            updated: Vec::new(),
28            schemas_applied: Vec::new(),
29            seeds_applied: Vec::new(),
30            disabled: Vec::new(),
31        }
32    }
33
34    pub fn add_valid(&mut self, name: String, version: String) {
35        self.valid.push((name, version));
36    }
37
38    pub fn add_installed(&mut self, name: String) {
39        self.installed.push(name);
40    }
41
42    pub fn add_updated(&mut self, name: String) {
43        self.updated.push(name);
44    }
45
46    pub fn add_schema_applied(&mut self, name: String) {
47        self.schemas_applied.push(name);
48    }
49
50    pub fn add_seed_applied(&mut self, name: String) {
51        self.seeds_applied.push(name);
52    }
53
54    pub fn add_disabled(&mut self, name: String) {
55        self.disabled.push(name);
56    }
57
58    pub fn total_active(&self) -> usize {
59        self.valid.len() + self.installed.len() + self.updated.len()
60    }
61
62    pub fn has_changes(&self) -> bool {
63        !self.installed.is_empty()
64            || !self.updated.is_empty()
65            || !self.schemas_applied.is_empty()
66            || !self.seeds_applied.is_empty()
67    }
68}
69
70impl Display for ValidationSummary {
71    fn display(&self) {
72        DisplayUtils::section_header("Module Validation Summary");
73
74        if !self.valid.is_empty() {
75            let displays: Vec<StatusDisplay> = self
76                .valid
77                .iter()
78                .map(|(name, version)| {
79                    let detail = format!("v{version}");
80                    StatusDisplay::new(ItemStatus::Valid, name).with_detail(detail)
81                })
82                .collect();
83
84            let collection = CollectionDisplay::new("Valid modules", displays);
85            collection.display();
86        }
87
88        if !self.installed.is_empty() {
89            let displays: Vec<StatusDisplay> = self
90                .installed
91                .iter()
92                .map(|name| StatusDisplay::new(ItemStatus::Applied, name))
93                .collect();
94
95            let collection = CollectionDisplay::new("Newly installed", displays);
96            collection.display();
97        }
98
99        if !self.updated.is_empty() {
100            let displays: Vec<StatusDisplay> = self
101                .updated
102                .iter()
103                .map(|name| StatusDisplay::new(ItemStatus::Applied, name))
104                .collect();
105
106            let collection = CollectionDisplay::new("Updated modules", displays);
107            collection.display();
108        }
109
110        if !self.schemas_applied.is_empty() {
111            let displays: Vec<StatusDisplay> = self
112                .schemas_applied
113                .iter()
114                .map(|name| StatusDisplay::new(ItemStatus::Applied, name))
115                .collect();
116
117            let collection = CollectionDisplay::new("Schemas applied", displays);
118            collection.display();
119        }
120
121        if !self.seeds_applied.is_empty() {
122            let displays: Vec<StatusDisplay> = self
123                .seeds_applied
124                .iter()
125                .map(|name| StatusDisplay::new(ItemStatus::Applied, name))
126                .collect();
127
128            let collection = CollectionDisplay::new("Seeds applied", displays);
129            collection.display();
130        }
131
132        if !self.disabled.is_empty() {
133            let displays: Vec<StatusDisplay> = self
134                .disabled
135                .iter()
136                .map(|name| StatusDisplay::new(ItemStatus::Disabled, name).with_detail("disabled"))
137                .collect();
138
139            let collection = CollectionDisplay::new("Disabled modules", displays);
140            collection.display();
141        }
142
143        let total_active = self.total_active();
144        if total_active > 0 {
145            let mut stderr = std::io::stderr();
146            writeln!(
147                stderr,
148                "\n{} {} active modules ready",
149                Theme::icon(MessageLevel::Success),
150                Theme::color(&total_active.to_string(), EmphasisType::Bold)
151            )
152            .ok();
153        }
154    }
155}
156
157impl Default for ValidationSummary {
158    fn default() -> Self {
159        Self::new()
160    }
161}
162
163#[derive(Debug, Clone)]
164pub struct OperationResult {
165    pub operation: String,
166    pub success: bool,
167    pub message: Option<String>,
168    pub details: Vec<String>,
169}
170
171impl OperationResult {
172    pub fn success(operation: impl Into<String>) -> Self {
173        Self {
174            operation: operation.into(),
175            success: true,
176            message: None,
177            details: Vec::new(),
178        }
179    }
180
181    pub fn failure(operation: impl Into<String>, message: impl Into<String>) -> Self {
182        Self {
183            operation: operation.into(),
184            success: false,
185            message: Some(message.into()),
186            details: Vec::new(),
187        }
188    }
189
190    #[must_use]
191    pub fn with_message(mut self, message: impl Into<String>) -> Self {
192        self.message = Some(message.into());
193        self
194    }
195
196    #[must_use]
197    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
198        self.details.push(detail.into());
199        self
200    }
201
202    #[must_use]
203    pub fn with_details(mut self, details: Vec<String>) -> Self {
204        self.details = details;
205        self
206    }
207}
208
209impl Display for OperationResult {
210    fn display(&self) {
211        let level = if self.success {
212            MessageLevel::Success
213        } else {
214            MessageLevel::Error
215        };
216
217        let base_message = self.message.as_ref().map_or_else(
218            || self.operation.clone(),
219            |msg| format!("{}: {msg}", self.operation),
220        );
221
222        DisplayUtils::message(level, &base_message);
223
224        for detail in &self.details {
225            let colored = Theme::color(detail, EmphasisType::Dim);
226            let mut stderr = std::io::stderr();
227            writeln!(stderr, "  \u{2022} {colored}").ok();
228        }
229    }
230}
231
232#[derive(Debug)]
233pub struct ProgressSummary {
234    pub total: usize,
235    pub completed: usize,
236    pub failed: usize,
237    pub operation_name: String,
238}
239
240impl ProgressSummary {
241    pub fn new(operation_name: impl Into<String>, total: usize) -> Self {
242        Self {
243            total,
244            completed: 0,
245            failed: 0,
246            operation_name: operation_name.into(),
247        }
248    }
249
250    pub fn add_success(&mut self) {
251        self.completed += 1;
252    }
253
254    pub fn add_failure(&mut self) {
255        self.failed += 1;
256    }
257
258    pub const fn is_complete(&self) -> bool {
259        self.completed + self.failed >= self.total
260    }
261
262    pub fn success_rate(&self) -> f64 {
263        if self.total == 0 {
264            1.0
265        } else {
266            let completed = u32::try_from(self.completed).unwrap_or(u32::MAX);
267            let total = u32::try_from(self.total).unwrap_or(u32::MAX);
268            f64::from(completed) / f64::from(total)
269        }
270    }
271}
272
273impl Display for ProgressSummary {
274    fn display(&self) {
275        let status = if self.failed == 0 {
276            MessageLevel::Success
277        } else if self.completed > 0 {
278            MessageLevel::Warning
279        } else {
280            MessageLevel::Error
281        };
282
283        let message = if self.failed > 0 {
284            format!(
285                "{}: {}/{} completed, {} failed",
286                self.operation_name, self.completed, self.total, self.failed
287            )
288        } else {
289            format!(
290                "{}: {}/{} completed",
291                self.operation_name, self.completed, self.total
292            )
293        };
294
295        DisplayUtils::message(status, &message);
296    }
297}