1use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7pub struct OutputManager {
9 pub parser: OutputParser,
11 pub compressor: SemanticCompressor,
13 _cache: HashMap<String, ParsedOutput>,
15}
16
17impl OutputManager {
18 pub fn new() -> Self {
20 Self {
21 parser: OutputParser::new(),
22 compressor: SemanticCompressor::new(),
23 _cache: HashMap::new(),
24 }
25 }
26
27 pub fn process_output(&mut self, raw_output: &str) -> Result<ProcessedOutput> {
29 let parsed = self.parser.parse(raw_output)?;
31
32 let highlights = self.extract_highlights(&parsed);
34
35 let compressed = if raw_output.len() > 1024 {
37 Some(self.compressor.compress(raw_output)?)
38 } else {
39 None
40 };
41
42 Ok(ProcessedOutput {
43 raw: raw_output.to_string(),
44 parsed: parsed.clone(),
45 highlights,
46 compressed,
47 timestamp: chrono::Utc::now(),
48 })
49 }
50
51 fn extract_highlights(&self, parsed: &ParsedOutput) -> Vec<Highlight> {
53 let mut highlights = Vec::new();
54
55 match parsed {
56 ParsedOutput::CodeExecution { result: _, metrics } => {
57 if metrics.execution_time > std::time::Duration::from_secs(5) {
58 highlights.push(Highlight {
59 category: HighlightCategory::Performance,
60 message: format!("Slow execution: {:?}", metrics.execution_time),
61 severity: Severity::Warning,
62 });
63 }
64 }
65 ParsedOutput::BuildOutput { status, .. } => match status {
66 BuildStatus::Failed(error) => {
67 highlights.push(Highlight {
68 category: HighlightCategory::Error,
69 message: error.clone(),
70 severity: Severity::Error,
71 });
72 }
73 BuildStatus::Warning(warning) => {
74 highlights.push(Highlight {
75 category: HighlightCategory::Warning,
76 message: warning.clone(),
77 severity: Severity::Warning,
78 });
79 }
80 _ => {}
81 },
82 ParsedOutput::TestResults { failed, .. } => {
83 if *failed > 0 {
84 highlights.push(Highlight {
85 category: HighlightCategory::TestFailure,
86 message: format!("{} tests failed", failed),
87 severity: Severity::Error,
88 });
89 }
90 }
91 ParsedOutput::StructuredLog { level, message, .. } => {
92 if matches!(level, LogLevel::Error | LogLevel::Warning) {
93 highlights.push(Highlight {
94 category: HighlightCategory::Log,
95 message: message.clone(),
96 severity: match level {
97 LogLevel::Error => Severity::Error,
98 LogLevel::Warning => Severity::Warning,
99 _ => Severity::Info,
100 },
101 });
102 }
103 }
104 _ => {}
105 }
106
107 highlights
108 }
109}
110
111impl Default for OutputManager {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117pub struct OutputParser {
119 patterns: HashMap<String, regex::Regex>,
121}
122
123impl OutputParser {
124 pub fn new() -> Self {
126 let mut patterns = HashMap::new();
127
128 patterns.insert(
130 "error".to_string(),
131 regex::Regex::new(r"(?i)(error|exception|failure)").unwrap(),
132 );
133 patterns.insert(
134 "warning".to_string(),
135 regex::Regex::new(r"(?i)(warning|warn)").unwrap(),
136 );
137 patterns.insert(
138 "success".to_string(),
139 regex::Regex::new(r"(?i)(success|passed|completed)").unwrap(),
140 );
141
142 Self { patterns }
143 }
144
145 pub fn parse(&self, output: &str) -> Result<ParsedOutput> {
147 if output.contains("BUILD SUCCESSFUL") || output.contains("Build succeeded") {
151 Ok(ParsedOutput::BuildOutput {
152 status: BuildStatus::Success,
153 artifacts: Vec::new(),
154 })
155 } else if output.contains("BUILD FAILED") || output.contains("Build failed") {
156 Ok(ParsedOutput::BuildOutput {
157 status: BuildStatus::Failed("Build failed".to_string()),
158 artifacts: Vec::new(),
159 })
160 } else if output.contains("tests passed") || output.contains("All tests passed") {
161 Ok(ParsedOutput::TestResults {
162 passed: 1, failed: 0,
164 details: TestDetails::default(),
165 })
166 } else if self.patterns["error"].is_match(output) {
167 Ok(ParsedOutput::StructuredLog {
168 level: LogLevel::Error,
169 message: output.to_string(),
170 context: LogContext::default(),
171 })
172 } else {
173 Ok(ParsedOutput::PlainText(output.to_string()))
174 }
175 }
176}
177
178impl Default for OutputParser {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184pub struct SemanticCompressor {
186 _compression_level: f32,
188}
189
190impl SemanticCompressor {
191 pub fn new() -> Self {
193 Self {
194 _compression_level: 0.5,
195 }
196 }
197
198 pub fn compress(&self, output: &str) -> Result<CompressedOutput> {
200 let compressed = if output.len() > 500 {
203 format!("{}... (truncated)", &output[..500])
204 } else {
205 output.to_string()
206 };
207
208 let compressed_len = compressed.len();
209 let original_len = output.len();
210
211 Ok(CompressedOutput {
212 original_size: original_len,
213 compressed_size: compressed_len,
214 content: compressed,
215 compression_ratio: compressed_len as f32 / original_len as f32,
216 })
217 }
218}
219
220impl Default for SemanticCompressor {
221 fn default() -> Self {
222 Self::new()
223 }
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub enum ParsedOutput {
229 PlainText(String),
231
232 CodeExecution {
234 result: String,
235 metrics: ExecutionMetrics,
236 },
237
238 BuildOutput {
240 status: BuildStatus,
241 artifacts: Vec<Artifact>,
242 },
243
244 TestResults {
246 passed: usize,
247 failed: usize,
248 details: TestDetails,
249 },
250
251 StructuredLog {
253 level: LogLevel,
254 message: String,
255 context: LogContext,
256 },
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ExecutionMetrics {
262 pub execution_time: std::time::Duration,
264 pub memory_usage: Option<usize>,
266 pub cpu_usage: Option<f32>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub enum BuildStatus {
273 Success,
274 Failed(String),
275 Warning(String),
276 InProgress,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct Artifact {
282 pub name: String,
284 pub path: String,
286 pub size: usize,
288}
289
290#[derive(Debug, Clone, Default, Serialize, Deserialize)]
292pub struct TestDetails {
293 pub suite: Option<String>,
295 pub duration: Option<std::time::Duration>,
297 pub failed_tests: Vec<String>,
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
303pub enum LogLevel {
304 Trace,
305 Debug,
306 Info,
307 Warning,
308 Error,
309}
310
311#[derive(Debug, Clone, Default, Serialize, Deserialize)]
313pub struct LogContext {
314 pub file: Option<String>,
316 pub line: Option<usize>,
318 pub fields: HashMap<String, serde_json::Value>,
320}
321
322#[derive(Debug, Clone)]
324pub struct ProcessedOutput {
325 pub raw: String,
327 pub parsed: ParsedOutput,
329 pub highlights: Vec<Highlight>,
331 pub compressed: Option<CompressedOutput>,
333 pub timestamp: chrono::DateTime<chrono::Utc>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct Highlight {
340 pub category: HighlightCategory,
342 pub message: String,
344 pub severity: Severity,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
350pub enum HighlightCategory {
351 Error,
352 Warning,
353 Performance,
354 TestFailure,
355 Log,
356 Success,
357}
358
359#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
361pub enum Severity {
362 Info,
363 Warning,
364 Error,
365 Critical,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct CompressedOutput {
371 pub original_size: usize,
373 pub compressed_size: usize,
375 pub content: String,
377 pub compression_ratio: f32,
379}
380
381use once_cell::sync::Lazy;
383static _REGEX_DEPENDENCY: Lazy<()> = Lazy::new(|| {
384 });
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_output_parser() {
393 let parser = OutputParser::new();
394
395 let output = "BUILD SUCCESSFUL";
396 let parsed = parser.parse(output).unwrap();
397
398 match parsed {
399 ParsedOutput::BuildOutput { status, .. } => {
400 assert!(matches!(status, BuildStatus::Success));
401 }
402 _ => panic!("Expected BuildOutput"),
403 }
404 }
405
406 #[test]
407 fn test_output_manager() {
408 let mut manager = OutputManager::new();
409
410 let output = "Error: Something went wrong";
411 let processed = manager.process_output(output).unwrap();
412
413 assert!(!processed.highlights.is_empty());
414 assert_eq!(processed.highlights[0].severity, Severity::Error);
415 }
416}