vkteams_bot_cli/commands/
mod.rs1use 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#[async_trait]
21pub trait Command {
22 async fn execute(&self, bot: &Bot) -> CliResult<()>;
24
25 async fn execute_with_output(&self, bot: &Bot, _output_format: &OutputFormat) -> CliResult<()> {
27 self.execute(bot).await
29 }
30
31 fn name(&self) -> &'static str;
33
34 fn validate(&self) -> CliResult<()> {
36 Ok(())
37 }
38}
39
40#[async_trait]
42pub trait CommandExecutor {
43 async fn execute_with_result(&self, bot: &Bot) -> CommandResult;
45
46 fn name(&self) -> &'static str;
48
49 fn validate(&self) -> CliResult<()> {
51 Ok(())
52 }
53}
54
55#[derive(clap::ValueEnum, Clone, Debug, Default, PartialEq)]
57pub enum OutputFormat {
58 #[default]
59 Pretty,
60 Json,
61 Table,
62 Quiet,
63}
64
65pub struct CommandContext {
67 pub bot: Bot,
68 pub verbose: bool,
69 pub output_format: OutputFormat,
70}
71
72#[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 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 self.display(&OutputFormat::Pretty)?;
147 }
148 OutputFormat::Quiet => {
149 if !self.success
151 && let Some(message) = &self.message
152 {
153 eprintln!("{message}");
154 }
155 }
156 }
157 Ok(())
158 }
159}
160#[derive(Subcommand, Debug, Clone)]
162pub enum Commands {
163 #[command(flatten)]
165 Messaging(messaging::MessagingCommands),
166
167 #[command(flatten)]
169 Chat(chat::ChatCommands),
170
171 #[command(flatten)]
173 Scheduling(scheduling::SchedulingCommands),
174
175 #[command(flatten)]
177 Config(config::ConfigCommands),
178
179 #[command(flatten)]
181 Diagnostic(diagnostic::DiagnosticCommands),
182
183 #[command(flatten)]
185 Files(files::FileCommands),
186
187 #[command(flatten)]
189 Storage(storage::StorageCommands),
190
191 #[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 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 assert!(res.display(&OutputFormat::Json).is_ok());
337 }
338}