1use crossterm::style::Stylize;
10
11use crate::cql_lexer::{self, TokenKind};
12use crate::driver::types::CqlValue;
13
14pub struct CqlColorizer {
16 enabled: bool,
17}
18
19impl CqlColorizer {
20 pub fn new(enabled: bool) -> Self {
22 Self { enabled }
23 }
24
25 pub fn is_enabled(&self) -> bool {
27 self.enabled
28 }
29
30 pub fn colorize_value(&self, value: &CqlValue) -> String {
39 if !self.enabled {
40 return value.to_string();
41 }
42 self.colorize_value_inner(value)
43 }
44
45 pub fn colorize_header(&self, name: &str) -> String {
47 if !self.enabled {
48 return name.to_string();
49 }
50 format!("{}", name.magenta().bold())
51 }
52
53 pub fn colorize_error(&self, msg: &str) -> String {
55 if !self.enabled {
56 return msg.to_string();
57 }
58 format!("{}", msg.red().bold())
59 }
60
61 pub fn colorize_warning(&self, msg: &str) -> String {
63 self.colorize_error(msg)
64 }
65
66 pub fn colorize_trace_label(&self, label: &str) -> String {
68 if !self.enabled {
69 return label.to_string();
70 }
71 format!("{}", label.magenta().bold())
72 }
73
74 pub fn colorize_cluster_name(&self, name: &str) -> String {
76 if !self.enabled {
77 return name.to_string();
78 }
79 format!("{}", name.blue().bold())
80 }
81
82 fn colorize_value_inner(&self, value: &CqlValue) -> String {
84 match value {
85 CqlValue::Ascii(s) | CqlValue::Text(s) => {
86 format!("{}", s.as_str().yellow().bold())
87 }
88 CqlValue::Int(_)
89 | CqlValue::BigInt(_)
90 | CqlValue::SmallInt(_)
91 | CqlValue::TinyInt(_)
92 | CqlValue::Float(_)
93 | CqlValue::Double(_)
94 | CqlValue::Decimal(_)
95 | CqlValue::Varint(_)
96 | CqlValue::Counter(_)
97 | CqlValue::Boolean(_)
98 | CqlValue::Uuid(_)
99 | CqlValue::TimeUuid(_)
100 | CqlValue::Timestamp(_)
101 | CqlValue::Date(_)
102 | CqlValue::Time(_)
103 | CqlValue::Duration { .. }
104 | CqlValue::Inet(_) => {
105 format!("{}", value.to_string().green().bold())
106 }
107 CqlValue::Blob(_) => {
108 format!("{}", value.to_string().dark_magenta())
109 }
110 CqlValue::Null => String::new(),
111 CqlValue::Unset => {
112 format!("{}", "<unset>".red().bold())
113 }
114 CqlValue::List(items) => {
115 let mut result = format!("{}", "[".blue().bold());
116 for (i, item) in items.iter().enumerate() {
117 if i > 0 {
118 result.push_str(&format!("{}", ", ".blue().bold()));
119 }
120 result.push_str(&self.colorize_collection_element(item));
121 }
122 result.push_str(&format!("{}", "]".blue().bold()));
123 result
124 }
125 CqlValue::Set(items) => {
126 let mut result = format!("{}", "{".blue().bold());
127 for (i, item) in items.iter().enumerate() {
128 if i > 0 {
129 result.push_str(&format!("{}", ", ".blue().bold()));
130 }
131 result.push_str(&self.colorize_collection_element(item));
132 }
133 result.push_str(&format!("{}", "}".blue().bold()));
134 result
135 }
136 CqlValue::Map(entries) => {
137 let mut result = format!("{}", "{".blue().bold());
138 for (i, (k, v)) in entries.iter().enumerate() {
139 if i > 0 {
140 result.push_str(&format!("{}", ", ".blue().bold()));
141 }
142 result.push_str(&self.colorize_collection_element(k));
143 result.push_str(&format!("{}", ": ".blue().bold()));
144 result.push_str(&self.colorize_collection_element(v));
145 }
146 result.push_str(&format!("{}", "}".blue().bold()));
147 result
148 }
149 CqlValue::Tuple(items) => {
150 let mut result = format!("{}", "(".blue().bold());
151 for (i, item) in items.iter().enumerate() {
152 if i > 0 {
153 result.push_str(&format!("{}", ", ".blue().bold()));
154 }
155 match item {
156 Some(v) => result.push_str(&self.colorize_collection_element(v)),
157 None => result.push_str(&format!("{}", "null".red().bold())),
158 }
159 }
160 result.push_str(&format!("{}", ")".blue().bold()));
161 result
162 }
163 CqlValue::UserDefinedType { fields, .. } => {
164 let mut result = format!("{}", "{".blue().bold());
165 for (i, (name, val)) in fields.iter().enumerate() {
166 if i > 0 {
167 result.push_str(&format!("{}", ", ".blue().bold()));
168 }
169 result.push_str(&format!("{}", name.as_str().yellow().bold()));
171 result.push_str(&format!("{}", ": ".blue().bold()));
172 match val {
173 Some(v) => result.push_str(&self.colorize_collection_element(v)),
174 None => result.push_str(&format!("{}", "null".red().bold())),
175 }
176 }
177 result.push_str(&format!("{}", "}".blue().bold()));
178 result
179 }
180 }
181 }
182
183 fn colorize_collection_element(&self, value: &CqlValue) -> String {
185 match value {
186 CqlValue::Ascii(s) | CqlValue::Text(s) => {
187 let quoted = format!("'{}'", s.replace('\'', "''"));
189 format!("{}", quoted.yellow().bold())
190 }
191 other => self.colorize_value_inner(other),
192 }
193 }
194
195 pub fn colorize_line(&self, line: &str) -> String {
204 if !self.enabled {
205 return line.to_string();
206 }
207
208 let tokens = cql_lexer::tokenize(line);
209 let mut result = String::with_capacity(line.len() * 2);
210
211 for token in &tokens {
212 match token.kind {
213 TokenKind::Keyword => {
214 result.push_str(&format!("{}", token.text.as_str().blue().bold()));
215 }
216 TokenKind::StringLiteral | TokenKind::DollarStringLiteral => {
217 result.push_str(&format!("{}", token.text.as_str().green()));
218 }
219 TokenKind::NumberLiteral => {
220 result.push_str(&format!("{}", token.text.as_str().cyan()));
221 }
222 TokenKind::BlobLiteral => {
223 result.push_str(&format!("{}", token.text.as_str().dark_magenta()));
224 }
225 TokenKind::UuidLiteral => {
226 result.push_str(&format!("{}", token.text.as_str().green()));
227 }
228 TokenKind::BooleanLiteral => {
229 result.push_str(&format!("{}", token.text.as_str().green().bold()));
230 }
231 TokenKind::LineComment | TokenKind::BlockComment => {
232 result.push_str(&format!("{}", token.text.as_str().dark_grey()));
233 }
234 _ => {
235 result.push_str(&token.text);
236 }
237 }
238 }
239
240 result
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn keywords_are_highlighted() {
250 let c = CqlColorizer::new(true);
251 let output = c.colorize_line("SELECT * FROM users");
252 assert!(output.contains("\x1b["), "should contain ANSI escape codes");
253 assert!(output.contains("SELECT"));
254 assert!(output.contains("FROM"));
255 }
256
257 #[test]
258 fn colorizer_disabled_returns_unchanged() {
259 let c = CqlColorizer::new(false);
260 let output = c.colorize_line("SELECT * FROM users");
261 assert_eq!(output, "SELECT * FROM users");
262 }
263
264 #[test]
265 fn string_literals_are_colored() {
266 let c = CqlColorizer::new(true);
267 let output = c.colorize_line("INSERT INTO t (a) VALUES ('hello')");
268 assert!(output.contains("\x1b["));
270 assert!(output.contains("hello"));
271 }
272
273 #[test]
274 fn numbers_are_colored() {
275 let c = CqlColorizer::new(true);
276 let output = c.colorize_line("SELECT * FROM t LIMIT 100");
277 assert!(output.contains("100"));
278 }
279
280 #[test]
281 fn comments_are_colored() {
282 let c = CqlColorizer::new(true);
283 let output = c.colorize_line("SELECT 1 -- test comment");
284 assert!(output.contains("test comment"));
285 }
286
287 #[test]
288 fn non_keywords_are_not_highlighted() {
289 let c = CqlColorizer::new(true);
290 let output = c.colorize_line("my_table");
291 assert!(!output.contains("\x1b["));
293 }
294
295 #[test]
296 fn mixed_case_keywords() {
297 let c = CqlColorizer::new(true);
298 let output = c.colorize_line("select * from users");
299 assert!(
300 output.contains("\x1b["),
301 "lowercase keywords should also be highlighted"
302 );
303 }
304
305 #[test]
306 fn identifiers_after_from_not_highlighted() {
307 let c = CqlColorizer::new(true);
308 let output = c.colorize_line("SELECT * FROM users");
310 assert!(output.ends_with("users"));
313 }
314
315 #[test]
316 fn keyword_names_as_identifiers_not_highlighted() {
317 let c = CqlColorizer::new(true);
318 let output_key = c.colorize_line("SELECT * FROM KEY");
320 assert!(
321 output_key.ends_with("KEY"),
322 "KEY after FROM should not be highlighted"
323 );
324 let output_set = c.colorize_line("SELECT * FROM SET");
325 assert!(
326 output_set.ends_with("SET"),
327 "SET after FROM should not be highlighted"
328 );
329 }
330
331 #[test]
332 fn qualified_name_after_dot_not_highlighted() {
333 let c = CqlColorizer::new(true);
334 let output = c.colorize_line("SELECT * FROM ks.users");
335 assert!(output.ends_with("users"));
337 }
338
339 #[test]
342 fn colorize_text_value_yellow() {
343 let c = CqlColorizer::new(true);
344 let output = c.colorize_value(&CqlValue::Text("hello".to_string()));
345 assert!(output.contains("\x1b["), "should contain ANSI codes");
346 assert!(output.contains("hello"));
347 }
348
349 #[test]
350 fn colorize_int_value_green() {
351 let c = CqlColorizer::new(true);
352 let output = c.colorize_value(&CqlValue::Int(42));
353 assert!(output.contains("\x1b["), "should contain ANSI codes");
354 assert!(output.contains("42"));
355 }
356
357 #[test]
358 fn colorize_null_value_empty() {
359 let c = CqlColorizer::new(true);
360 let output = c.colorize_value(&CqlValue::Null);
361 assert_eq!(output, "");
362 }
363
364 #[test]
365 fn colorize_blob_value_dark_magenta() {
366 let c = CqlColorizer::new(true);
367 let output = c.colorize_value(&CqlValue::Blob(vec![0xde, 0xad]));
368 assert!(output.contains("\x1b["), "should contain ANSI codes");
369 assert!(output.contains("dead"));
370 }
371
372 #[test]
373 fn colorize_list_with_blue_delimiters() {
374 let c = CqlColorizer::new(true);
375 let list = CqlValue::List(vec![CqlValue::Int(1), CqlValue::Int(2)]);
376 let output = c.colorize_value(&list);
377 assert!(output.contains("\x1b["), "should contain ANSI codes");
378 }
379
380 #[test]
381 fn colorize_value_disabled_returns_plain() {
382 let c = CqlColorizer::new(false);
383 let output = c.colorize_value(&CqlValue::Text("hello".to_string()));
384 assert_eq!(output, "hello");
385 }
386
387 #[test]
388 fn colorize_header_magenta() {
389 let c = CqlColorizer::new(true);
390 let output = c.colorize_header("name");
391 assert!(output.contains("\x1b["), "should contain ANSI codes");
392 assert!(output.contains("name"));
393 }
394
395 #[test]
396 fn colorize_error_red() {
397 let c = CqlColorizer::new(true);
398 let output = c.colorize_error("SyntaxException: bad input");
399 assert!(output.contains("\x1b["), "should contain ANSI codes");
400 assert!(output.contains("SyntaxException"));
401 }
402
403 #[test]
404 fn colorize_map_with_colored_elements() {
405 let c = CqlColorizer::new(true);
406 let map = CqlValue::Map(vec![(CqlValue::Text("key".to_string()), CqlValue::Int(42))]);
407 let output = c.colorize_value(&map);
408 assert!(output.contains("\x1b["), "should contain ANSI codes");
409 }
410
411 #[test]
412 fn colorize_udt_field_names_yellow() {
413 let c = CqlColorizer::new(true);
414 let udt = CqlValue::UserDefinedType {
415 keyspace: "ks".to_string(),
416 type_name: "my_type".to_string(),
417 fields: vec![
418 (
419 "name".to_string(),
420 Some(CqlValue::Text("Alice".to_string())),
421 ),
422 ("age".to_string(), Some(CqlValue::Int(30))),
423 ],
424 };
425 let output = c.colorize_value(&udt);
426 assert!(output.contains("\x1b["), "should contain ANSI codes");
427 }
428}