1use std::io::{self, Write};
2
3use serde::Serialize;
4
5use crate::cli::{OutputFormat, ProfileCommand};
6use crate::error::{CliError, Result};
7use crate::models::{Column, DataType, Row, Value};
8use crate::output::formatter::{create_formatter, Formatter};
9use crate::tui::renderer::render_output;
10
11use super::config::{ConnectionType, ExecutionScope, LocalConfig, Profile, ProfileManager};
12
13#[derive(Debug, Serialize)]
14pub struct ProfileListOutput {
15 pub profiles: Vec<ProfileListItem>,
16}
17
18#[derive(Debug, Serialize, Clone)]
19pub struct ProfileListItem {
20 pub name: String,
21 pub data_dir: String,
22 pub is_default: bool,
23}
24
25#[derive(Debug, Serialize)]
26pub struct ProfileShowOutput {
27 pub name: String,
28 pub data_dir: String,
29 pub is_default: bool,
30}
31
32pub fn execute_profile_command(cmd: ProfileCommand, output: OutputFormat) -> Result<()> {
33 match cmd {
34 ProfileCommand::Create { name, data_dir } => {
35 let mut manager = ProfileManager::load()?;
36 manager.create(
37 &name,
38 Profile {
39 connection_type: ConnectionType::Local,
40 local: Some(LocalConfig {
41 path: data_dir.clone(),
42 }),
43 server: None,
44 data_dir: Some(data_dir),
45 execution_scope: ExecutionScope::Local,
46 cluster_read: None,
47 },
48 )?;
49 manager.save()?;
50 let (columns, rows) = status_columns_rows("OK", format!("Created profile '{name}'."));
51 let mut writer = io::stdout().lock();
52 write_rows_with_formatter_to(&mut writer, output, &columns, &rows)
53 }
54 ProfileCommand::List => {
55 let manager = ProfileManager::load()?;
56 let items = build_list_items(&manager);
57 output_profile_list(&items, output)
58 }
59 ProfileCommand::Show { name } => {
60 let manager = ProfileManager::load()?;
61 let show = build_show_output(&manager, &name)?;
62 output_profile_show(&show, output)
63 }
64 ProfileCommand::Delete { name } => {
65 let mut manager = ProfileManager::load()?;
66 manager.delete(&name)?;
67 manager.save()?;
68 let (columns, rows) = status_columns_rows("OK", format!("Deleted profile '{name}'."));
69 let mut writer = io::stdout().lock();
70 write_rows_with_formatter_to(&mut writer, output, &columns, &rows)
71 }
72 ProfileCommand::SetDefault { name } => {
73 let mut manager = ProfileManager::load()?;
74 manager.set_default(&name)?;
75 manager.save()?;
76 let (columns, rows) =
77 status_columns_rows("OK", format!("Set default profile to '{name}'."));
78 let mut writer = io::stdout().lock();
79 write_rows_with_formatter_to(&mut writer, output, &columns, &rows)
80 }
81 }
82}
83
84pub fn execute_profile_tui<'a>(
85 cmd: ProfileCommand,
86 connection_label: impl Into<String>,
87 output_format: OutputFormat,
88 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
89) -> Result<()> {
90 let mut admin_launcher = admin_launcher;
91 let context_message = Some(profile_command_context(&cmd));
92 match cmd {
93 ProfileCommand::Create { name, data_dir } => {
94 let mut manager = ProfileManager::load()?;
95 manager.create(
96 &name,
97 Profile {
98 connection_type: ConnectionType::Local,
99 local: Some(LocalConfig {
100 path: data_dir.clone(),
101 }),
102 server: None,
103 data_dir: Some(data_dir),
104 execution_scope: ExecutionScope::Local,
105 cluster_read: None,
106 },
107 )?;
108 manager.save()?;
109 let (columns, rows) = status_columns_rows("OK", format!("Created profile '{name}'."));
110 render_output(
111 columns,
112 rows,
113 connection_label,
114 context_message,
115 true,
116 None,
117 output_format,
118 admin_launcher.take(),
119 )
120 }
121 ProfileCommand::List => {
122 let manager = ProfileManager::load()?;
123 let items = build_list_items(&manager);
124 let (columns, rows) = list_columns_rows(&items);
125 render_output(
126 columns,
127 rows,
128 connection_label,
129 context_message,
130 true,
131 None,
132 output_format,
133 admin_launcher.take(),
134 )
135 }
136 ProfileCommand::Show { name } => {
137 let manager = ProfileManager::load()?;
138 let show = build_show_output(&manager, &name)?;
139 let (columns, rows) = show_columns_rows(&show);
140 render_output(
141 columns,
142 rows,
143 connection_label,
144 context_message,
145 true,
146 None,
147 output_format,
148 admin_launcher.take(),
149 )
150 }
151 ProfileCommand::Delete { name } => {
152 let mut manager = ProfileManager::load()?;
153 manager.delete(&name)?;
154 manager.save()?;
155 let (columns, rows) = status_columns_rows("OK", format!("Deleted profile '{name}'."));
156 render_output(
157 columns,
158 rows,
159 connection_label,
160 context_message,
161 true,
162 None,
163 output_format,
164 admin_launcher.take(),
165 )
166 }
167 ProfileCommand::SetDefault { name } => {
168 let mut manager = ProfileManager::load()?;
169 manager.set_default(&name)?;
170 manager.save()?;
171 let (columns, rows) =
172 status_columns_rows("OK", format!("Set default profile to '{name}'."));
173 render_output(
174 columns,
175 rows,
176 connection_label,
177 context_message,
178 true,
179 None,
180 output_format,
181 admin_launcher.take(),
182 )
183 }
184 }
185}
186
187fn build_list_items(manager: &ProfileManager) -> Vec<ProfileListItem> {
188 let default_name = manager.default_profile();
189 manager
190 .list()
191 .into_iter()
192 .filter_map(|name| {
193 manager.get(name).map(|profile| ProfileListItem {
194 name: name.to_string(),
195 data_dir: profile.local_path().unwrap_or_else(|| "-".to_string()),
196 is_default: default_name == Some(name),
197 })
198 })
199 .collect()
200}
201
202fn build_show_output(manager: &ProfileManager, name: &str) -> Result<ProfileShowOutput> {
203 let profile = manager
204 .get(name)
205 .ok_or_else(|| CliError::ProfileNotFound(name.to_string()))?;
206 let data_dir = profile.local_path().unwrap_or_else(|| "-".to_string());
207 Ok(ProfileShowOutput {
208 name: name.to_string(),
209 data_dir,
210 is_default: manager.default_profile() == Some(name),
211 })
212}
213
214fn output_profile_list(items: &[ProfileListItem], output: OutputFormat) -> Result<()> {
215 match output {
216 OutputFormat::Table => write_list_table(items),
217 OutputFormat::Json => write_list_json(items),
218 _ => Err(CliError::InvalidArgument(format!(
219 "Unsupported output format for profile list: {:?}",
220 output
221 ))),
222 }
223}
224
225fn output_profile_show(show: &ProfileShowOutput, output: OutputFormat) -> Result<()> {
226 match output {
227 OutputFormat::Table => write_show_table(show),
228 OutputFormat::Json => write_show_json(show),
229 _ => Err(CliError::InvalidArgument(format!(
230 "Unsupported output format for profile show: {:?}",
231 output
232 ))),
233 }
234}
235
236fn profile_command_context(cmd: &ProfileCommand) -> String {
237 match cmd {
238 ProfileCommand::Create { name, .. } => format!("profile create {name}"),
239 ProfileCommand::List => "profile list".to_string(),
240 ProfileCommand::Show { name } => format!("profile show {name}"),
241 ProfileCommand::Delete { name } => format!("profile delete {name}"),
242 ProfileCommand::SetDefault { name } => format!("profile set-default {name}"),
243 }
244}
245
246fn write_list_table(items: &[ProfileListItem]) -> Result<()> {
247 let mut writer = io::stdout().lock();
248 write_list_table_to(&mut writer, items)
249}
250
251fn write_list_table_to(writer: &mut dyn Write, items: &[ProfileListItem]) -> Result<()> {
252 let (columns, rows) = list_columns_rows(items);
253 write_rows_with_formatter_to(writer, OutputFormat::Table, &columns, &rows)
254}
255
256fn write_list_json(items: &[ProfileListItem]) -> Result<()> {
257 let mut writer = io::stdout().lock();
258 write_list_json_to(&mut writer, items)
259}
260
261fn write_list_json_to(writer: &mut dyn Write, items: &[ProfileListItem]) -> Result<()> {
262 let output = ProfileListOutput {
263 profiles: items.to_vec(),
264 };
265 let value = serde_json::to_value(output)?;
266 write_json_value_to(writer, &value)
267}
268
269fn write_show_table(show: &ProfileShowOutput) -> Result<()> {
270 let mut writer = io::stdout().lock();
271 write_show_table_to(&mut writer, show)
272}
273
274fn write_show_table_to(writer: &mut dyn Write, show: &ProfileShowOutput) -> Result<()> {
275 let (columns, rows) = show_columns_rows(show);
276 let mut formatter = KeyValueFormatter::new();
277 formatter.write_header(writer, &columns)?;
278 for row in &rows {
279 formatter.write_row(writer, row)?;
280 }
281 formatter.write_footer(writer)
282}
283
284fn write_show_json(show: &ProfileShowOutput) -> Result<()> {
285 let mut writer = io::stdout().lock();
286 write_show_json_to(&mut writer, show)
287}
288
289fn write_show_json_to(writer: &mut dyn Write, show: &ProfileShowOutput) -> Result<()> {
290 let columns = vec![
291 Column::new("name", DataType::Text),
292 Column::new("data_dir", DataType::Text),
293 Column::new("is_default", DataType::Bool),
294 ];
295 let rows = vec![Row::new(vec![
296 Value::Text(show.name.clone()),
297 Value::Text(show.data_dir.clone()),
298 Value::Bool(show.is_default),
299 ])];
300 let array = rows_to_json_array(&columns, &rows)?;
301 let obj = array
302 .as_array()
303 .and_then(|items| items.first())
304 .cloned()
305 .unwrap_or(serde_json::Value::Null);
306 write_json_value_to(writer, &obj)
307}
308
309fn list_columns_rows(items: &[ProfileListItem]) -> (Vec<Column>, Vec<Row>) {
310 let columns = vec![
311 Column::new("Name", DataType::Text),
312 Column::new("Data Dir", DataType::Text),
313 Column::new("Default", DataType::Text),
314 ];
315 let rows = items
316 .iter()
317 .map(|item| {
318 Row::new(vec![
319 Value::Text(item.name.clone()),
320 Value::Text(item.data_dir.clone()),
321 Value::Text(if item.is_default { "*" } else { "" }.to_string()),
322 ])
323 })
324 .collect();
325 (columns, rows)
326}
327
328fn show_columns_rows(show: &ProfileShowOutput) -> (Vec<Column>, Vec<Row>) {
329 let columns = vec![
330 Column::new("Name", DataType::Text),
331 Column::new("Data Dir", DataType::Text),
332 Column::new("Default", DataType::Text),
333 ];
334 let rows = vec![Row::new(vec![
335 Value::Text(show.name.clone()),
336 Value::Text(show.data_dir.clone()),
337 Value::Text(if show.is_default { "Yes" } else { "No" }.to_string()),
338 ])];
339 (columns, rows)
340}
341
342fn status_columns_rows(status: &str, message: String) -> (Vec<Column>, Vec<Row>) {
343 let columns = vec![
344 Column::new("Status", DataType::Text),
345 Column::new("Message", DataType::Text),
346 ];
347 let rows = vec![Row::new(vec![
348 Value::Text(status.to_string()),
349 Value::Text(message),
350 ])];
351 (columns, rows)
352}
353
354fn write_rows_with_formatter_to(
355 writer: &mut dyn Write,
356 output: OutputFormat,
357 columns: &[Column],
358 rows: &[Row],
359) -> Result<()> {
360 let mut formatter = create_formatter(output);
361 formatter.write_header(writer, columns)?;
362 for row in rows {
363 formatter.write_row(writer, row)?;
364 }
365 formatter.write_footer(writer)
366}
367
368fn rows_to_json_array(columns: &[Column], rows: &[Row]) -> Result<serde_json::Value> {
369 let mut buffer = Vec::new();
370 let mut formatter = create_formatter(OutputFormat::Json);
371 formatter.write_header(&mut buffer, columns)?;
372 for row in rows {
373 formatter.write_row(&mut buffer, row)?;
374 }
375 formatter.write_footer(&mut buffer)?;
376 let value: serde_json::Value = serde_json::from_slice(&buffer)?;
377 Ok(value)
378}
379
380fn write_json_value_to(writer: &mut dyn Write, value: &serde_json::Value) -> Result<()> {
381 serde_json::to_writer_pretty(&mut *writer, value)?;
382 writeln!(writer)?;
383 Ok(())
384}
385
386struct KeyValueFormatter {
387 columns: Vec<String>,
388 rows: Vec<Row>,
389}
390
391impl KeyValueFormatter {
392 fn new() -> Self {
393 Self {
394 columns: Vec::new(),
395 rows: Vec::new(),
396 }
397 }
398}
399
400impl Formatter for KeyValueFormatter {
401 fn write_header(&mut self, _writer: &mut dyn Write, columns: &[Column]) -> Result<()> {
402 self.columns = columns.iter().map(|column| column.name.clone()).collect();
403 Ok(())
404 }
405
406 fn write_row(&mut self, _writer: &mut dyn Write, row: &Row) -> Result<()> {
407 self.rows.push(row.clone());
408 Ok(())
409 }
410
411 fn write_footer(&mut self, writer: &mut dyn Write) -> Result<()> {
412 const LABEL_WIDTH: usize = 9;
413 if let Some(row) = self.rows.first() {
414 for (column, value) in self.columns.iter().zip(row.columns.iter()) {
415 let label = format!("{}:", column);
416 let value_str = format_value(value);
417 writeln!(
418 writer,
419 "{:<width$} {}",
420 label,
421 value_str,
422 width = LABEL_WIDTH
423 )?;
424 }
425 }
426 Ok(())
427 }
428
429 fn supports_streaming(&self) -> bool {
430 false
431 }
432}
433
434fn format_value(value: &Value) -> String {
435 match value {
436 Value::Null => "NULL".to_string(),
437 Value::Bool(b) => {
438 if *b {
439 "Yes".to_string()
440 } else {
441 "No".to_string()
442 }
443 }
444 Value::Int(i) => i.to_string(),
445 Value::Float(f) => format!("{:.6}", f),
446 Value::Text(s) => s.clone(),
447 Value::Bytes(bytes) => format!("{:?}", bytes),
448 Value::Vector(values) => format!("{:?}", values),
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455
456 fn sample_items() -> Vec<ProfileListItem> {
457 vec![
458 ProfileListItem {
459 name: "dev".to_string(),
460 data_dir: "/path/dev".to_string(),
461 is_default: true,
462 },
463 ProfileListItem {
464 name: "prod".to_string(),
465 data_dir: "/path/prod".to_string(),
466 is_default: false,
467 },
468 ]
469 }
470
471 fn sample_show() -> ProfileShowOutput {
472 ProfileShowOutput {
473 name: "dev".to_string(),
474 data_dir: "/path/dev".to_string(),
475 is_default: true,
476 }
477 }
478
479 #[test]
480 fn test_list_table_output() {
481 let items = sample_items();
482 let mut buffer = Vec::new();
483 write_list_table_to(&mut buffer, &items).unwrap();
484 let output = String::from_utf8(buffer).unwrap();
485 assert!(output.contains("Name"));
486 assert!(output.contains("Data Dir"));
487 assert!(output.contains("Default"));
488 assert!(output.contains("dev"));
489 assert!(output.contains("/path/dev"));
490 assert!(output.contains("*"));
491 }
492
493 #[test]
494 fn test_list_json_output() {
495 let items = sample_items();
496 let mut buffer = Vec::new();
497 write_list_json_to(&mut buffer, &items).unwrap();
498 let value: serde_json::Value = serde_json::from_slice(&buffer).unwrap();
499 let profiles = value["profiles"].as_array().unwrap();
500 assert_eq!(profiles.len(), 2);
501 assert!(profiles.iter().any(|profile| {
502 profile["name"] == "dev"
503 && profile["data_dir"] == "/path/dev"
504 && profile["is_default"] == true
505 }));
506 assert!(profiles.iter().any(|profile| {
507 profile["name"] == "prod"
508 && profile["data_dir"] == "/path/prod"
509 && profile["is_default"] == false
510 }));
511 }
512
513 #[test]
514 fn test_show_table_output() {
515 let show = sample_show();
516 let mut buffer = Vec::new();
517 write_show_table_to(&mut buffer, &show).unwrap();
518 let output = String::from_utf8(buffer).unwrap();
519 assert!(output.contains("Name:"));
520 assert!(output.contains("Data Dir:"));
521 assert!(output.contains("Default:"));
522 assert!(output.contains("dev"));
523 assert!(output.contains("/path/dev"));
524 assert!(output.contains("Yes"));
525 }
526
527 #[test]
528 fn test_show_json_output() {
529 let show = sample_show();
530 let mut buffer = Vec::new();
531 write_show_json_to(&mut buffer, &show).unwrap();
532 let value: serde_json::Value = serde_json::from_slice(&buffer).unwrap();
533 assert_eq!(value["name"], "dev");
534 assert_eq!(value["data_dir"], "/path/dev");
535 assert_eq!(value["is_default"], true);
536 }
537}