Skip to main content

alopex_cli/profile/
commands.rs

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}