systemprompt_logging/services/cli/
summary.rs1use 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}