vkteams_bot_cli/commands/
mod.rs

1//! CLI Commands module
2//!
3//! This module contains all command implementations organized by functionality.
4
5use crate::errors::prelude::Result as CliResult;
6use async_trait::async_trait;
7use clap::Subcommand;
8use vkteams_bot::prelude::Bot;
9
10pub mod chat;
11pub mod config;
12pub mod daemon;
13pub mod diagnostic;
14pub mod files;
15pub mod messaging;
16pub mod scheduling;
17pub mod storage;
18
19/// Trait that all CLI commands must implement
20#[async_trait]
21pub trait Command {
22    /// Execute the command
23    async fn execute(&self, bot: &Bot) -> CliResult<()>;
24
25    /// Execute the command with structured output support (optional implementation)
26    async fn execute_with_output(&self, bot: &Bot, _output_format: &OutputFormat) -> CliResult<()> {
27        // Default implementation falls back to legacy execute method
28        self.execute(bot).await
29    }
30
31    /// Get command name for logging
32    fn name(&self) -> &'static str;
33
34    /// Validate command arguments before execution
35    fn validate(&self) -> CliResult<()> {
36        Ok(())
37    }
38}
39
40/// New trait for commands that return structured results
41#[async_trait]
42pub trait CommandExecutor {
43    /// Execute the command and return structured result
44    async fn execute_with_result(&self, bot: &Bot) -> CommandResult;
45
46    /// Get command name for logging
47    fn name(&self) -> &'static str;
48
49    /// Validate command arguments before execution
50    fn validate(&self) -> CliResult<()> {
51        Ok(())
52    }
53}
54
55/// Output format options
56#[derive(clap::ValueEnum, Clone, Debug, Default, PartialEq)]
57pub enum OutputFormat {
58    #[default]
59    Pretty,
60    Json,
61    Table,
62    Quiet,
63}
64
65/// Context passed to commands for execution
66pub struct CommandContext {
67    pub bot: Bot,
68    pub verbose: bool,
69    pub output_format: OutputFormat,
70}
71
72/// Command execution result with optional output
73#[derive(serde::Serialize)]
74pub struct CommandResult {
75    pub success: bool,
76    pub message: Option<String>,
77    pub data: Option<serde_json::Value>,
78}
79
80impl CommandResult {
81    pub fn success() -> Self {
82        Self {
83            success: true,
84            message: None,
85            data: None,
86        }
87    }
88
89    pub fn success_with_message(message: impl Into<String>) -> Self {
90        Self {
91            success: true,
92            message: Some(message.into()),
93            data: None,
94        }
95    }
96
97    pub fn success_with_data(data: serde_json::Value) -> Self {
98        Self {
99            success: true,
100            message: None,
101            data: Some(data),
102        }
103    }
104
105    pub fn error(message: impl Into<String>) -> Self {
106        Self {
107            success: false,
108            message: Some(message.into()),
109            data: None,
110        }
111    }
112
113    /// Display the command result according to the specified output format
114    pub fn display(&self, format: &OutputFormat) -> crate::errors::prelude::Result<()> {
115        use crate::constants::ui::emoji;
116        use colored::Colorize;
117
118        match format {
119            OutputFormat::Pretty => {
120                if self.success {
121                    if let Some(message) = &self.message {
122                        println!("{} {}", emoji::CHECK, message.green());
123                    }
124                    if let Some(data) = &self.data {
125                        let json_str = serde_json::to_string_pretty(data).map_err(|e| {
126                            crate::errors::prelude::CliError::UnexpectedError(format!(
127                                "Failed to serialize data: {e}"
128                            ))
129                        })?;
130                        println!("{}", json_str.green());
131                    }
132                } else if let Some(message) = &self.message {
133                    eprintln!("{} {}", emoji::CROSS, message.red());
134                }
135            }
136            OutputFormat::Json => {
137                let json_output = serde_json::to_string_pretty(self).map_err(|e| {
138                    crate::errors::prelude::CliError::UnexpectedError(format!(
139                        "Failed to serialize result: {e}"
140                    ))
141                })?;
142                println!("{json_output}");
143            }
144            OutputFormat::Table => {
145                // For table format, fall back to pretty format for now
146                self.display(&OutputFormat::Pretty)?;
147            }
148            OutputFormat::Quiet => {
149                // No output in quiet mode unless it's an error
150                if !self.success
151                    && let Some(message) = &self.message
152                {
153                    eprintln!("{message}");
154                }
155            }
156        }
157        Ok(())
158    }
159}
160/// All available CLI commands
161#[derive(Subcommand, Debug, Clone)]
162pub enum Commands {
163    // Messaging commands
164    #[command(flatten)]
165    Messaging(messaging::MessagingCommands),
166
167    // Chat management commands
168    #[command(flatten)]
169    Chat(chat::ChatCommands),
170
171    // Scheduling commands
172    #[command(flatten)]
173    Scheduling(scheduling::SchedulingCommands),
174
175    // Configuration commands
176    #[command(flatten)]
177    Config(config::ConfigCommands),
178
179    // Diagnostic commands
180    #[command(flatten)]
181    Diagnostic(diagnostic::DiagnosticCommands),
182
183    // File management commands
184    #[command(flatten)]
185    Files(files::FileCommands),
186
187    // Storage and database commands
188    #[command(flatten)]
189    Storage(storage::StorageCommands),
190
191    // Daemon management commands
192    #[command(flatten)]
193    Daemon(daemon::DaemonCommands),
194}
195
196#[async_trait]
197impl Command for Commands {
198    async fn execute(&self, bot: &Bot) -> CliResult<()> {
199        match self {
200            Commands::Messaging(cmd) => cmd.execute(bot).await,
201            Commands::Chat(cmd) => cmd.execute(bot).await,
202            Commands::Scheduling(cmd) => cmd.execute(bot).await,
203            Commands::Config(cmd) => cmd.execute(bot).await,
204            Commands::Diagnostic(cmd) => cmd.execute(bot).await,
205            Commands::Files(cmd) => cmd.execute(bot).await,
206            Commands::Storage(cmd) => cmd.execute(bot).await,
207            Commands::Daemon(cmd) => cmd.execute(bot).await,
208        }
209    }
210
211    fn name(&self) -> &'static str {
212        match self {
213            Commands::Messaging(cmd) => cmd.name(),
214            Commands::Chat(cmd) => cmd.name(),
215            Commands::Scheduling(cmd) => cmd.name(),
216            Commands::Config(cmd) => Command::name(cmd),
217            Commands::Diagnostic(cmd) => cmd.name(),
218            Commands::Files(cmd) => cmd.name(),
219            Commands::Storage(cmd) => cmd.name(),
220            Commands::Daemon(cmd) => cmd.name(),
221        }
222    }
223
224    fn validate(&self) -> CliResult<()> {
225        match self {
226            Commands::Messaging(cmd) => cmd.validate(),
227            Commands::Chat(cmd) => cmd.validate(),
228            Commands::Scheduling(cmd) => cmd.validate(),
229            Commands::Config(cmd) => Command::validate(cmd),
230            Commands::Diagnostic(cmd) => cmd.validate(),
231            Commands::Files(cmd) => cmd.validate(),
232            Commands::Storage(cmd) => cmd.validate(),
233            Commands::Daemon(cmd) => cmd.validate(),
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use serde_json::json;
242
243    #[test]
244    fn test_command_result_success() {
245        let res = CommandResult::success();
246        assert!(res.success);
247        assert!(res.message.is_none());
248        assert!(res.data.is_none());
249    }
250
251    #[test]
252    fn test_command_result_success_with_message() {
253        let res = CommandResult::success_with_message("ok");
254        assert!(res.success);
255        assert_eq!(res.message.as_deref(), Some("ok"));
256        assert!(res.data.is_none());
257    }
258
259    #[test]
260    fn test_command_result_success_with_data() {
261        let data = json!({"key": 1});
262        let res = CommandResult::success_with_data(data.clone());
263        assert!(res.success);
264        assert!(res.message.is_none());
265        assert_eq!(res.data, Some(data));
266    }
267
268    #[test]
269    fn test_command_result_error() {
270        let res = CommandResult::error("fail");
271        assert!(!res.success);
272        assert_eq!(res.message.as_deref(), Some("fail"));
273        assert!(res.data.is_none());
274    }
275
276    #[test]
277    fn test_command_result_display_pretty() {
278        let res = CommandResult::success_with_message("done");
279        assert!(res.display(&OutputFormat::Pretty).is_ok());
280        let res = CommandResult::error("fail");
281        assert!(res.display(&OutputFormat::Pretty).is_ok());
282        let res = CommandResult::success_with_data(json!({"a": 1}));
283        assert!(res.display(&OutputFormat::Pretty).is_ok());
284    }
285
286    #[test]
287    fn test_command_result_display_json() {
288        let res = CommandResult::success_with_message("done");
289        assert!(res.display(&OutputFormat::Json).is_ok());
290        let res = CommandResult::error("fail");
291        assert!(res.display(&OutputFormat::Json).is_ok());
292        let res = CommandResult::success_with_data(json!({"a": 1}));
293        assert!(res.display(&OutputFormat::Json).is_ok());
294    }
295
296    #[test]
297    fn test_command_result_display_table() {
298        let res = CommandResult::success_with_message("done");
299        assert!(res.display(&OutputFormat::Table).is_ok());
300    }
301
302    #[test]
303    fn test_command_result_display_quiet() {
304        let res = CommandResult::success_with_message("done");
305        assert!(res.display(&OutputFormat::Quiet).is_ok());
306        let res = CommandResult::error("fail");
307        assert!(res.display(&OutputFormat::Quiet).is_ok());
308    }
309
310    #[test]
311    fn test_output_format_default() {
312        let f = OutputFormat::default();
313        assert_eq!(f, OutputFormat::Pretty);
314    }
315
316    #[test]
317    fn test_command_result_display_json_error() {
318        // Создаём CommandResult с несерилизуемым data (например, с циклической ссылкой невозможно, но можно подменить тип)
319        // Здесь просто проверяем, что ошибка сериализации корректно обрабатывается
320        struct NotSerializable;
321        impl serde::Serialize for NotSerializable {
322            fn serialize<S>(&self, _: S) -> Result<S::Ok, S::Error>
323            where
324                S: serde::Serializer,
325            {
326                Err(serde::ser::Error::custom("fail"))
327            }
328        }
329        let data = serde_json::to_value(NotSerializable).unwrap_or(json!(null));
330        let res = CommandResult {
331            success: true,
332            message: None,
333            data: Some(data),
334        };
335        // display не упадёт, просто выведет null
336        assert!(res.display(&OutputFormat::Json).is_ok());
337    }
338}