nirv_engine/cli/
output_formatter.rs1use colored::*;
2use serde_json::{json, Value as JsonValue};
3use base64::prelude::*;
4use crate::utils::types::{QueryResult, Value};
5use crate::cli::cli_args::OutputFormat;
6
7pub struct OutputFormatter;
9
10impl OutputFormatter {
11 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 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 let mut col_widths = vec![0; result.columns.len()];
30
31 for (i, col) in result.columns.iter().enumerate() {
33 col_widths[i] = col.name.len();
34 }
35
36 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 for width in &mut col_widths {
48 *width = (*width).max(8);
49 }
50
51 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 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 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 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 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 fn format_csv(result: &QueryResult) -> String {
149 let mut output = String::new();
150
151 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 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 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 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 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 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 pub fn format_error(error: &crate::utils::error::NirvError) -> String {
235 format!("{} {}", "Error:".red().bold(), error.to_string().red())
236 }
237
238 pub fn format_success(message: &str) -> String {
240 format!("{} {}", "Success:".green().bold(), message)
241 }
242
243 pub fn format_info(message: &str) -> String {
245 format!("{} {}", "Info:".blue().bold(), message)
246 }
247}