nirv_engine/cli/
output_formatter.rs

1use colored::*;
2use serde_json::{json, Value as JsonValue};
3use base64::prelude::*;
4use crate::utils::types::{QueryResult, Value};
5use crate::cli::cli_args::OutputFormat;
6
7/// Formats query results for CLI output
8pub struct OutputFormatter;
9
10impl OutputFormatter {
11    /// Format query results according to the specified format
12    pub fn format_result(result: &QueryResult, format: &OutputFormat) -> String {
13        match format {
14            OutputFormat::Table => Self::format_table(result),
15            OutputFormat::Json => Self::format_json(result),
16            OutputFormat::Csv => Self::format_csv(result),
17        }
18    }
19    
20    /// Format results as a colored table
21    fn format_table(result: &QueryResult) -> String {
22        if result.is_empty() {
23            return "No results found.".dimmed().to_string();
24        }
25        
26        let mut output = String::new();
27        
28        // Calculate column widths
29        let mut col_widths = vec![0; result.columns.len()];
30        
31        // Header widths
32        for (i, col) in result.columns.iter().enumerate() {
33            col_widths[i] = col.name.len();
34        }
35        
36        // Data widths
37        for row in &result.rows {
38            for (i, value) in row.values.iter().enumerate() {
39                if i < col_widths.len() {
40                    let value_str = Self::value_to_string(value);
41                    col_widths[i] = col_widths[i].max(value_str.len());
42                }
43            }
44        }
45        
46        // Ensure minimum width
47        for width in &mut col_widths {
48            *width = (*width).max(8);
49        }
50        
51        // Header
52        output.push_str(&Self::format_table_separator(&col_widths, true));
53        output.push('|');
54        for (i, col) in result.columns.iter().enumerate() {
55            output.push_str(&format!(" {:<width$} |", 
56                col.name.bold().cyan(), 
57                width = col_widths[i]
58            ));
59        }
60        output.push('\n');
61        output.push_str(&Self::format_table_separator(&col_widths, false));
62        
63        // Data rows
64        for row in &result.rows {
65            output.push('|');
66            for (i, value) in row.values.iter().enumerate() {
67                if i < col_widths.len() {
68                    let formatted_value = Self::format_value_colored(value);
69                    output.push_str(&format!(" {:<width$} |", 
70                        formatted_value, 
71                        width = col_widths[i]
72                    ));
73                }
74            }
75            output.push('\n');
76        }
77        
78        output.push_str(&Self::format_table_separator(&col_widths, true));
79        
80        // Footer with metadata
81        output.push_str(&format!("\n{} {} in {:.2}ms\n", 
82            result.row_count().to_string().green().bold(),
83            if result.row_count() == 1 { "row" } else { "rows" },
84            result.execution_time.as_millis()
85        ));
86        
87        output
88    }
89    
90    /// Format table separator line
91    fn format_table_separator(col_widths: &[usize], is_border: bool) -> String {
92        let mut separator = String::new();
93        
94        if is_border {
95            separator.push('+');
96            for &width in col_widths {
97                separator.push_str(&"-".repeat(width + 2));
98                separator.push('+');
99            }
100        } else {
101            separator.push('|');
102            for &width in col_widths {
103                separator.push_str(&"-".repeat(width + 2));
104                separator.push('|');
105            }
106        }
107        
108        separator.push('\n');
109        separator
110    }
111    
112    /// Format results as JSON
113    fn format_json(result: &QueryResult) -> String {
114        let mut rows = Vec::new();
115        
116        for row in &result.rows {
117            let mut row_obj = serde_json::Map::new();
118            
119            for (i, value) in row.values.iter().enumerate() {
120                if let Some(col) = result.columns.get(i) {
121                    let json_value = Self::value_to_json(value);
122                    row_obj.insert(col.name.clone(), json_value);
123                }
124            }
125            
126            rows.push(JsonValue::Object(row_obj));
127        }
128        
129        let output = json!({
130            "data": rows,
131            "metadata": {
132                "columns": result.columns.iter().map(|col| {
133                    json!({
134                        "name": col.name,
135                        "type": format!("{:?}", col.data_type),
136                        "nullable": col.nullable
137                    })
138                }).collect::<Vec<_>>(),
139                "row_count": result.row_count(),
140                "execution_time_ms": result.execution_time.as_millis()
141            }
142        });
143        
144        serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
145    }
146    
147    /// Format results as CSV
148    fn format_csv(result: &QueryResult) -> String {
149        let mut output = String::new();
150        
151        // Header
152        let headers: Vec<String> = result.columns.iter()
153            .map(|col| Self::escape_csv_field(&col.name))
154            .collect();
155        output.push_str(&headers.join(","));
156        output.push('\n');
157        
158        // Data rows
159        for row in &result.rows {
160            let values: Vec<String> = row.values.iter()
161                .map(|value| Self::escape_csv_field(&Self::value_to_string(value)))
162                .collect();
163            output.push_str(&values.join(","));
164            output.push('\n');
165        }
166        
167        output
168    }
169    
170    /// Convert a Value to a display string
171    fn value_to_string(value: &Value) -> String {
172        match value {
173            Value::Text(s) => s.clone(),
174            Value::Integer(i) => i.to_string(),
175            Value::Float(f) => format!("{:.2}", f),
176            Value::Boolean(b) => b.to_string(),
177            Value::Date(d) => d.clone(),
178            Value::DateTime(dt) => dt.clone(),
179            Value::Json(j) => j.clone(),
180            Value::Binary(b) => format!("<binary: {} bytes>", b.len()),
181            Value::Null => "NULL".to_string(),
182        }
183    }
184    
185    /// Convert a Value to a colored string for table display
186    fn format_value_colored(value: &Value) -> ColoredString {
187        match value {
188            Value::Text(s) => s.normal(),
189            Value::Integer(i) => i.to_string().blue(),
190            Value::Float(f) => format!("{:.2}", f).blue(),
191            Value::Boolean(true) => "true".green(),
192            Value::Boolean(false) => "false".red(),
193            Value::Date(d) => d.yellow(),
194            Value::DateTime(dt) => dt.yellow(),
195            Value::Json(j) => j.magenta(),
196            Value::Binary(b) => format!("<binary: {} bytes>", b.len()).cyan(),
197            Value::Null => "NULL".dimmed(),
198        }
199    }
200    
201    /// Convert a Value to JSON
202    fn value_to_json(value: &Value) -> JsonValue {
203        match value {
204            Value::Text(s) => JsonValue::String(s.clone()),
205            Value::Integer(i) => JsonValue::Number((*i).into()),
206            Value::Float(f) => {
207                if let Some(num) = serde_json::Number::from_f64(*f) {
208                    JsonValue::Number(num)
209                } else {
210                    JsonValue::Null
211                }
212            },
213            Value::Boolean(b) => JsonValue::Bool(*b),
214            Value::Date(d) => JsonValue::String(d.clone()),
215            Value::DateTime(dt) => JsonValue::String(dt.clone()),
216            Value::Json(j) => {
217                serde_json::from_str(j).unwrap_or(JsonValue::String(j.clone()))
218            },
219            Value::Binary(b) => JsonValue::String(BASE64_STANDARD.encode(b)),
220            Value::Null => JsonValue::Null,
221        }
222    }
223    
224    /// Escape CSV field if it contains special characters
225    fn escape_csv_field(field: &str) -> String {
226        if field.contains(',') || field.contains('"') || field.contains('\n') {
227            format!("\"{}\"", field.replace('"', "\"\""))
228        } else {
229            field.to_string()
230        }
231    }
232    
233    /// Format error message for CLI display
234    pub fn format_error(error: &crate::utils::error::NirvError) -> String {
235        format!("{} {}", "Error:".red().bold(), error.to_string().red())
236    }
237    
238    /// Format success message for CLI display
239    pub fn format_success(message: &str) -> String {
240        format!("{} {}", "Success:".green().bold(), message)
241    }
242    
243    /// Format info message for CLI display
244    pub fn format_info(message: &str) -> String {
245        format!("{} {}", "Info:".blue().bold(), message)
246    }
247}