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};
9
10use super::config::{ConnectionType, LocalConfig, Profile, ProfileManager};
11
12#[derive(Debug, Serialize)]
13pub struct ProfileListOutput {
14    pub profiles: Vec<ProfileListItem>,
15}
16
17#[derive(Debug, Serialize, Clone)]
18pub struct ProfileListItem {
19    pub name: String,
20    pub data_dir: String,
21    pub is_default: bool,
22}
23
24#[derive(Debug, Serialize)]
25pub struct ProfileShowOutput {
26    pub name: String,
27    pub data_dir: String,
28    pub is_default: bool,
29}
30
31pub fn execute_profile_command(cmd: ProfileCommand, output: OutputFormat) -> Result<()> {
32    match cmd {
33        ProfileCommand::Create { name, data_dir } => {
34            let mut manager = ProfileManager::load()?;
35            manager.create(
36                &name,
37                Profile {
38                    connection_type: ConnectionType::Local,
39                    local: Some(LocalConfig {
40                        path: data_dir.clone(),
41                    }),
42                    server: None,
43                    data_dir: Some(data_dir),
44                },
45            )?;
46            manager.save()
47        }
48        ProfileCommand::List => {
49            let manager = ProfileManager::load()?;
50            let items = build_list_items(&manager);
51            output_profile_list(&items, output)
52        }
53        ProfileCommand::Show { name } => {
54            let manager = ProfileManager::load()?;
55            let show = build_show_output(&manager, &name)?;
56            output_profile_show(&show, output)
57        }
58        ProfileCommand::Delete { name } => {
59            let mut manager = ProfileManager::load()?;
60            manager.delete(&name)?;
61            manager.save()
62        }
63        ProfileCommand::SetDefault { name } => {
64            let mut manager = ProfileManager::load()?;
65            manager.set_default(&name)?;
66            manager.save()
67        }
68    }
69}
70
71fn build_list_items(manager: &ProfileManager) -> Vec<ProfileListItem> {
72    let default_name = manager.default_profile();
73    manager
74        .list()
75        .into_iter()
76        .filter_map(|name| {
77            manager.get(name).map(|profile| ProfileListItem {
78                name: name.to_string(),
79                data_dir: profile.local_path().unwrap_or_else(|| "-".to_string()),
80                is_default: default_name == Some(name),
81            })
82        })
83        .collect()
84}
85
86fn build_show_output(manager: &ProfileManager, name: &str) -> Result<ProfileShowOutput> {
87    let profile = manager
88        .get(name)
89        .ok_or_else(|| CliError::ProfileNotFound(name.to_string()))?;
90    let data_dir = profile.local_path().unwrap_or_else(|| "-".to_string());
91    Ok(ProfileShowOutput {
92        name: name.to_string(),
93        data_dir,
94        is_default: manager.default_profile() == Some(name),
95    })
96}
97
98fn output_profile_list(items: &[ProfileListItem], output: OutputFormat) -> Result<()> {
99    match output {
100        OutputFormat::Table => write_list_table(items),
101        OutputFormat::Json => write_list_json(items),
102        _ => Err(CliError::InvalidArgument(format!(
103            "Unsupported output format for profile list: {:?}",
104            output
105        ))),
106    }
107}
108
109fn output_profile_show(show: &ProfileShowOutput, output: OutputFormat) -> Result<()> {
110    match output {
111        OutputFormat::Table => write_show_table(show),
112        OutputFormat::Json => write_show_json(show),
113        _ => Err(CliError::InvalidArgument(format!(
114            "Unsupported output format for profile show: {:?}",
115            output
116        ))),
117    }
118}
119
120fn write_list_table(items: &[ProfileListItem]) -> Result<()> {
121    let mut writer = io::stdout().lock();
122    write_list_table_to(&mut writer, items)
123}
124
125fn write_list_table_to(writer: &mut dyn Write, items: &[ProfileListItem]) -> Result<()> {
126    let columns = vec![
127        Column::new("Name", DataType::Text),
128        Column::new("Data Dir", DataType::Text),
129        Column::new("Default", DataType::Text),
130    ];
131    let rows: Vec<Row> = items
132        .iter()
133        .map(|item| {
134            Row::new(vec![
135                Value::Text(item.name.clone()),
136                Value::Text(item.data_dir.clone()),
137                Value::Text(if item.is_default { "*" } else { "" }.to_string()),
138            ])
139        })
140        .collect();
141    write_rows_with_formatter_to(writer, OutputFormat::Table, &columns, &rows)
142}
143
144fn write_list_json(items: &[ProfileListItem]) -> Result<()> {
145    let mut writer = io::stdout().lock();
146    write_list_json_to(&mut writer, items)
147}
148
149fn write_list_json_to(writer: &mut dyn Write, items: &[ProfileListItem]) -> Result<()> {
150    let output = ProfileListOutput {
151        profiles: items.to_vec(),
152    };
153    let value = serde_json::to_value(output)?;
154    write_json_value_to(writer, &value)
155}
156
157fn write_show_table(show: &ProfileShowOutput) -> Result<()> {
158    let mut writer = io::stdout().lock();
159    write_show_table_to(&mut writer, show)
160}
161
162fn write_show_table_to(writer: &mut dyn Write, show: &ProfileShowOutput) -> Result<()> {
163    let columns = vec![
164        Column::new("Name", DataType::Text),
165        Column::new("Data Dir", DataType::Text),
166        Column::new("Default", DataType::Text),
167    ];
168    let rows = vec![Row::new(vec![
169        Value::Text(show.name.clone()),
170        Value::Text(show.data_dir.clone()),
171        Value::Text(if show.is_default { "Yes" } else { "No" }.to_string()),
172    ])];
173    let mut formatter = KeyValueFormatter::new();
174    formatter.write_header(writer, &columns)?;
175    for row in &rows {
176        formatter.write_row(writer, row)?;
177    }
178    formatter.write_footer(writer)
179}
180
181fn write_show_json(show: &ProfileShowOutput) -> Result<()> {
182    let mut writer = io::stdout().lock();
183    write_show_json_to(&mut writer, show)
184}
185
186fn write_show_json_to(writer: &mut dyn Write, show: &ProfileShowOutput) -> Result<()> {
187    let columns = vec![
188        Column::new("name", DataType::Text),
189        Column::new("data_dir", DataType::Text),
190        Column::new("is_default", DataType::Bool),
191    ];
192    let rows = vec![Row::new(vec![
193        Value::Text(show.name.clone()),
194        Value::Text(show.data_dir.clone()),
195        Value::Bool(show.is_default),
196    ])];
197    let array = rows_to_json_array(&columns, &rows)?;
198    let obj = array
199        .as_array()
200        .and_then(|items| items.first())
201        .cloned()
202        .unwrap_or(serde_json::Value::Null);
203    write_json_value_to(writer, &obj)
204}
205
206fn write_rows_with_formatter_to(
207    writer: &mut dyn Write,
208    output: OutputFormat,
209    columns: &[Column],
210    rows: &[Row],
211) -> Result<()> {
212    let mut formatter = create_formatter(output);
213    formatter.write_header(writer, columns)?;
214    for row in rows {
215        formatter.write_row(writer, row)?;
216    }
217    formatter.write_footer(writer)
218}
219
220fn rows_to_json_array(columns: &[Column], rows: &[Row]) -> Result<serde_json::Value> {
221    let mut buffer = Vec::new();
222    let mut formatter = create_formatter(OutputFormat::Json);
223    formatter.write_header(&mut buffer, columns)?;
224    for row in rows {
225        formatter.write_row(&mut buffer, row)?;
226    }
227    formatter.write_footer(&mut buffer)?;
228    let value: serde_json::Value = serde_json::from_slice(&buffer)?;
229    Ok(value)
230}
231
232fn write_json_value_to(writer: &mut dyn Write, value: &serde_json::Value) -> Result<()> {
233    serde_json::to_writer_pretty(&mut *writer, value)?;
234    writeln!(writer)?;
235    Ok(())
236}
237
238struct KeyValueFormatter {
239    columns: Vec<String>,
240    rows: Vec<Row>,
241}
242
243impl KeyValueFormatter {
244    fn new() -> Self {
245        Self {
246            columns: Vec::new(),
247            rows: Vec::new(),
248        }
249    }
250}
251
252impl Formatter for KeyValueFormatter {
253    fn write_header(&mut self, _writer: &mut dyn Write, columns: &[Column]) -> Result<()> {
254        self.columns = columns.iter().map(|column| column.name.clone()).collect();
255        Ok(())
256    }
257
258    fn write_row(&mut self, _writer: &mut dyn Write, row: &Row) -> Result<()> {
259        self.rows.push(row.clone());
260        Ok(())
261    }
262
263    fn write_footer(&mut self, writer: &mut dyn Write) -> Result<()> {
264        const LABEL_WIDTH: usize = 9;
265        if let Some(row) = self.rows.first() {
266            for (column, value) in self.columns.iter().zip(row.columns.iter()) {
267                let label = format!("{}:", column);
268                let value_str = format_value(value);
269                writeln!(
270                    writer,
271                    "{:<width$} {}",
272                    label,
273                    value_str,
274                    width = LABEL_WIDTH
275                )?;
276            }
277        }
278        Ok(())
279    }
280
281    fn supports_streaming(&self) -> bool {
282        false
283    }
284}
285
286fn format_value(value: &Value) -> String {
287    match value {
288        Value::Null => "NULL".to_string(),
289        Value::Bool(b) => {
290            if *b {
291                "Yes".to_string()
292            } else {
293                "No".to_string()
294            }
295        }
296        Value::Int(i) => i.to_string(),
297        Value::Float(f) => format!("{:.6}", f),
298        Value::Text(s) => s.clone(),
299        Value::Bytes(bytes) => format!("{:?}", bytes),
300        Value::Vector(values) => format!("{:?}", values),
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    fn sample_items() -> Vec<ProfileListItem> {
309        vec![
310            ProfileListItem {
311                name: "dev".to_string(),
312                data_dir: "/path/dev".to_string(),
313                is_default: true,
314            },
315            ProfileListItem {
316                name: "prod".to_string(),
317                data_dir: "/path/prod".to_string(),
318                is_default: false,
319            },
320        ]
321    }
322
323    fn sample_show() -> ProfileShowOutput {
324        ProfileShowOutput {
325            name: "dev".to_string(),
326            data_dir: "/path/dev".to_string(),
327            is_default: true,
328        }
329    }
330
331    #[test]
332    fn test_list_table_output() {
333        let items = sample_items();
334        let mut buffer = Vec::new();
335        write_list_table_to(&mut buffer, &items).unwrap();
336        let output = String::from_utf8(buffer).unwrap();
337        assert!(output.contains("Name"));
338        assert!(output.contains("Data Dir"));
339        assert!(output.contains("Default"));
340        assert!(output.contains("dev"));
341        assert!(output.contains("/path/dev"));
342        assert!(output.contains("*"));
343    }
344
345    #[test]
346    fn test_list_json_output() {
347        let items = sample_items();
348        let mut buffer = Vec::new();
349        write_list_json_to(&mut buffer, &items).unwrap();
350        let value: serde_json::Value = serde_json::from_slice(&buffer).unwrap();
351        let profiles = value["profiles"].as_array().unwrap();
352        assert_eq!(profiles.len(), 2);
353        assert!(profiles.iter().any(|profile| {
354            profile["name"] == "dev"
355                && profile["data_dir"] == "/path/dev"
356                && profile["is_default"] == true
357        }));
358        assert!(profiles.iter().any(|profile| {
359            profile["name"] == "prod"
360                && profile["data_dir"] == "/path/prod"
361                && profile["is_default"] == false
362        }));
363    }
364
365    #[test]
366    fn test_show_table_output() {
367        let show = sample_show();
368        let mut buffer = Vec::new();
369        write_show_table_to(&mut buffer, &show).unwrap();
370        let output = String::from_utf8(buffer).unwrap();
371        assert!(output.contains("Name:"));
372        assert!(output.contains("Data Dir:"));
373        assert!(output.contains("Default:"));
374        assert!(output.contains("dev"));
375        assert!(output.contains("/path/dev"));
376        assert!(output.contains("Yes"));
377    }
378
379    #[test]
380    fn test_show_json_output() {
381        let show = sample_show();
382        let mut buffer = Vec::new();
383        write_show_json_to(&mut buffer, &show).unwrap();
384        let value: serde_json::Value = serde_json::from_slice(&buffer).unwrap();
385        assert_eq!(value["name"], "dev");
386        assert_eq!(value["data_dir"], "/path/dev");
387        assert_eq!(value["is_default"], true);
388    }
389}