Skip to main content

lean_ctx/core/patterns/
ruby.rs

1pub fn compress(cmd_lower: &str, output: &str) -> Option<String> {
2    if cmd_lower.contains("rubocop") {
3        return compress_rubocop(output);
4    }
5    if cmd_lower.contains("bundle install") || cmd_lower.contains("bundle update") {
6        return compress_bundle(output);
7    }
8    if cmd_lower.contains("rake test") || cmd_lower.contains("rails test") {
9        return compress_minitest(output);
10    }
11    None
12}
13
14fn compress_rubocop(output: &str) -> Option<String> {
15    let mut offenses = Vec::new();
16    let mut files_inspected = 0u32;
17    let mut total_offenses = 0u32;
18
19    for line in output.lines() {
20        let trimmed = line.trim();
21        if trimmed.contains("files inspected") {
22            for word in trimmed.split_whitespace() {
23                if let Ok(n) = word.parse::<u32>() {
24                    files_inspected = n;
25                    break;
26                }
27            }
28            if let Some(pos) = trimmed.find("offense") {
29                let before = &trimmed[..pos];
30                for word in before.split(", ").last().unwrap_or("").split_whitespace() {
31                    if let Ok(n) = word.parse::<u32>() {
32                        total_offenses = n;
33                    }
34                }
35            }
36        } else if trimmed.contains(": C:")
37            || trimmed.contains(": W:")
38            || trimmed.contains(": E:")
39            || trimmed.contains(": F:")
40        {
41            offenses.push(trimmed.to_string());
42        }
43    }
44
45    if files_inspected == 0 && offenses.is_empty() {
46        return None;
47    }
48
49    let mut result = format!("rubocop: {files_inspected} files, {total_offenses} offenses");
50
51    if total_offenses == 0 {
52        result.push_str(" (clean)");
53        return Some(result);
54    }
55
56    let grouped = group_by_cop(&offenses);
57    for (cop, count) in grouped.iter().take(10) {
58        result.push_str(&format!("\n  {cop}: {count}x"));
59    }
60
61    if offenses.len() > 10 {
62        result.push_str(&format!("\n  ... +{} more", offenses.len() - 10));
63    }
64
65    Some(result)
66}
67
68fn group_by_cop(offenses: &[String]) -> Vec<(String, usize)> {
69    let mut map = std::collections::HashMap::new();
70    for offense in offenses {
71        let cop = offense
72            .split('[')
73            .next_back()
74            .and_then(|s| s.strip_suffix(']'))
75            .unwrap_or("unknown")
76            .to_string();
77        *map.entry(cop).or_insert(0usize) += 1;
78    }
79    let mut sorted: Vec<_> = map.into_iter().collect();
80    sorted.sort_by(|a, b| b.1.cmp(&a.1));
81    sorted
82}
83
84fn compress_bundle(output: &str) -> Option<String> {
85    let mut installed = 0u32;
86    let mut using = 0u32;
87
88    for line in output.lines() {
89        let trimmed = line.trim();
90        if trimmed.starts_with("Installing ") {
91            installed += 1;
92        } else if trimmed.starts_with("Using ") {
93            using += 1;
94        }
95    }
96
97    if installed == 0 && using == 0 {
98        return None;
99    }
100
101    let mut result = String::from("bundle: ");
102    if installed > 0 {
103        result.push_str(&format!("{installed} installed"));
104    }
105    if using > 0 {
106        if installed > 0 {
107            result.push_str(", ");
108        }
109        result.push_str(&format!("{using} using (cached)"));
110    }
111
112    for line in output.lines().rev().take(3) {
113        let trimmed = line.trim();
114        if trimmed.starts_with("Bundle complete") || trimmed.starts_with("Bundled gems") {
115            result.push_str(&format!("\n  {trimmed}"));
116            break;
117        }
118    }
119
120    Some(result)
121}
122
123fn compress_minitest(output: &str) -> Option<String> {
124    let mut total = 0u32;
125    let mut failures = 0u32;
126    let mut errors = 0u32;
127    let mut skips = 0u32;
128    let mut time = String::new();
129    let mut failure_details = Vec::new();
130
131    for line in output.lines() {
132        let trimmed = line.trim();
133        if trimmed.contains("runs,") && trimmed.contains("assertions") {
134            for part in trimmed.split(", ") {
135                let part = part.trim();
136                if part.ends_with("runs") {
137                    if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
138                        total = n;
139                    }
140                } else if part.ends_with("failures") {
141                    if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
142                        failures = n;
143                    }
144                } else if part.ends_with("errors") {
145                    if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
146                        errors = n;
147                    }
148                } else if part.ends_with("skips") {
149                    if let Some(n) = part.split_whitespace().next().and_then(|w| w.parse().ok()) {
150                        skips = n;
151                    }
152                }
153            }
154            if let Some(pos) = trimmed.find(" in ") {
155                time = trimmed[pos + 4..]
156                    .split(',')
157                    .next()
158                    .unwrap_or("")
159                    .trim()
160                    .to_string();
161            }
162        }
163        if trimmed.starts_with("Failure:") || trimmed.starts_with("Error:") {
164            failure_details.push(trimmed.to_string());
165        }
166    }
167
168    if total == 0 {
169        return None;
170    }
171
172    let passed = total
173        .saturating_sub(failures)
174        .saturating_sub(errors)
175        .saturating_sub(skips);
176    let mut result = format!("minitest: {passed} passed");
177    if failures > 0 {
178        result.push_str(&format!(", {failures} failed"));
179    }
180    if errors > 0 {
181        result.push_str(&format!(", {errors} errors"));
182    }
183    if skips > 0 {
184        result.push_str(&format!(", {skips} skipped"));
185    }
186    if !time.is_empty() {
187        result.push_str(&format!(" ({time})"));
188    }
189
190    for detail in failure_details.iter().take(5) {
191        result.push_str(&format!("\n  {detail}"));
192    }
193
194    Some(result)
195}