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