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