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
70fn display_applied_group(label: &str, names: &[String]) {
71    if names.is_empty() {
72        return;
73    }
74    let displays: Vec<StatusDisplay> = names
75        .iter()
76        .map(|name| StatusDisplay::new(ItemStatus::Applied, name))
77        .collect();
78    CollectionDisplay::new(label, displays).display();
79}
80
81impl Display for ValidationSummary {
82    fn display(&self) {
83        DisplayUtils::section_header("Module Validation Summary");
84
85        if !self.valid.is_empty() {
86            let displays: Vec<StatusDisplay> = self
87                .valid
88                .iter()
89                .map(|(name, version)| {
90                    StatusDisplay::new(ItemStatus::Valid, name).with_detail(format!("v{version}"))
91                })
92                .collect();
93            CollectionDisplay::new("Valid modules", displays).display();
94        }
95
96        display_applied_group("Newly installed", &self.installed);
97        display_applied_group("Updated modules", &self.updated);
98        display_applied_group("Schemas applied", &self.schemas_applied);
99        display_applied_group("Seeds applied", &self.seeds_applied);
100
101        if !self.disabled.is_empty() {
102            let displays: Vec<StatusDisplay> = self
103                .disabled
104                .iter()
105                .map(|name| StatusDisplay::new(ItemStatus::Disabled, name).with_detail("disabled"))
106                .collect();
107            CollectionDisplay::new("Disabled modules", displays).display();
108        }
109
110        let total_active = self.total_active();
111        if total_active > 0 {
112            let mut stderr = std::io::stderr();
113            writeln!(
114                stderr,
115                "\n{} {} active modules ready",
116                Theme::icon(MessageLevel::Success),
117                Theme::color(&total_active.to_string(), EmphasisType::Bold)
118            )
119            .ok();
120        }
121    }
122}
123
124impl Default for ValidationSummary {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130#[derive(Debug, Clone)]
131pub struct OperationResult {
132    pub operation: String,
133    pub success: bool,
134    pub message: Option<String>,
135    pub details: Vec<String>,
136}
137
138impl OperationResult {
139    pub fn success(operation: impl Into<String>) -> Self {
140        Self {
141            operation: operation.into(),
142            success: true,
143            message: None,
144            details: Vec::new(),
145        }
146    }
147
148    pub fn failure(operation: impl Into<String>, message: impl Into<String>) -> Self {
149        Self {
150            operation: operation.into(),
151            success: false,
152            message: Some(message.into()),
153            details: Vec::new(),
154        }
155    }
156
157    #[must_use]
158    pub fn with_message(mut self, message: impl Into<String>) -> Self {
159        self.message = Some(message.into());
160        self
161    }
162
163    #[must_use]
164    pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
165        self.details.push(detail.into());
166        self
167    }
168
169    #[must_use]
170    pub fn with_details(mut self, details: Vec<String>) -> Self {
171        self.details = details;
172        self
173    }
174}
175
176impl Display for OperationResult {
177    fn display(&self) {
178        let level = if self.success {
179            MessageLevel::Success
180        } else {
181            MessageLevel::Error
182        };
183
184        let base_message = self.message.as_ref().map_or_else(
185            || self.operation.clone(),
186            |msg| format!("{}: {msg}", self.operation),
187        );
188
189        DisplayUtils::message(level, &base_message);
190
191        for detail in &self.details {
192            let colored = Theme::color(detail, EmphasisType::Dim);
193            let mut stderr = std::io::stderr();
194            writeln!(stderr, "  \u{2022} {colored}").ok();
195        }
196    }
197}
198
199#[derive(Debug)]
200pub struct ProgressSummary {
201    pub total: usize,
202    pub completed: usize,
203    pub failed: usize,
204    pub operation_name: String,
205}
206
207impl ProgressSummary {
208    pub fn new(operation_name: impl Into<String>, total: usize) -> Self {
209        Self {
210            total,
211            completed: 0,
212            failed: 0,
213            operation_name: operation_name.into(),
214        }
215    }
216
217    pub fn add_success(&mut self) {
218        self.completed += 1;
219    }
220
221    pub fn add_failure(&mut self) {
222        self.failed += 1;
223    }
224
225    pub const fn is_complete(&self) -> bool {
226        self.completed + self.failed >= self.total
227    }
228
229    pub fn success_rate(&self) -> f64 {
230        if self.total == 0 {
231            1.0
232        } else {
233            let completed = u32::try_from(self.completed).unwrap_or(u32::MAX);
234            let total = u32::try_from(self.total).unwrap_or(u32::MAX);
235            f64::from(completed) / f64::from(total)
236        }
237    }
238}
239
240impl Display for ProgressSummary {
241    fn display(&self) {
242        let status = if self.failed == 0 {
243            MessageLevel::Success
244        } else if self.completed > 0 {
245            MessageLevel::Warning
246        } else {
247            MessageLevel::Error
248        };
249
250        let message = if self.failed > 0 {
251            format!(
252                "{}: {}/{} completed, {} failed",
253                self.operation_name, self.completed, self.total, self.failed
254            )
255        } else {
256            format!(
257                "{}: {}/{} completed",
258                self.operation_name, self.completed, self.total
259            )
260        };
261
262        DisplayUtils::message(status, &message);
263    }
264}