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, 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}