diode_base/command.rs
1//! Command-line interface framework for diode applications.
2//!
3//! This module provides a framework for building CLI applications with multiple subcommands
4//! that can access the dependency injection container. Commands are registered with the
5//! application and can be executed through a unified CLI interface.
6//!
7//! # Core Concepts
8//!
9//! - **Command**: A trait for defining CLI subcommands
10//! - **CommandRegistry**: Container for all registered commands
11//! - **CLI Integration**: Automatic integration with clap for argument parsing
12//!
13//! # Examples
14//!
15//! Basic command implementation:
16//!
17//! ```rust
18//! use diode_base::{Command, AddCommandExt};
19//! use diode::App;
20//! use clap::{ArgMatches, Command as ClapCommand};
21//! use std::process::ExitCode;
22//! use std::sync::Arc;
23//!
24//! struct HelloCommand;
25//!
26//! impl Command for HelloCommand {
27//! fn command() -> ClapCommand {
28//! ClapCommand::new("hello")
29//! .about("Prints a greeting")
30//! }
31//!
32//! async fn main(_app: Arc<App>, _matches: ArgMatches) -> ExitCode {
33//! println!("Hello, World!");
34//! ExitCode::SUCCESS
35//! }
36//! }
37//! ```
38
39use std::any::TypeId;
40use std::collections::{BTreeMap, HashMap};
41use std::marker::PhantomData;
42use std::mem::take;
43use std::process::ExitCode;
44use std::sync::Arc;
45
46use async_trait::async_trait;
47use clap::{Arg, ArgAction, ArgMatches};
48use diode::{App, AppBuilder};
49
50use crate::{CancellationToken, Config, Metrics, RunDaemonsExt, Tracing};
51
52/// Trait for defining CLI commands that can access the application's dependency container.
53///
54/// Commands are subcommands in the CLI that can perform operations using services
55/// and components from the application. Each command defines its CLI interface
56/// and main execution logic.
57///
58/// # Examples
59///
60/// Simple command:
61///
62/// ```rust
63/// use diode_base::Command;
64/// use diode::App;
65/// use clap::{ArgMatches, Command as ClapCommand};
66/// use std::process::ExitCode;
67/// use std::sync::Arc;
68///
69/// struct StatusCommand;
70///
71/// impl Command for StatusCommand {
72/// fn command() -> ClapCommand {
73/// ClapCommand::new("status")
74/// .about("Shows application status")
75/// }
76///
77/// async fn main(app: Arc<App>, _matches: ArgMatches) -> ExitCode {
78/// // Access services from the app container
79/// println!("Application is running");
80/// ExitCode::SUCCESS
81/// }
82/// }
83/// ```
84///
85/// Command with arguments:
86///
87/// ```rust
88/// use diode_base::Command;
89/// use diode::App;
90/// use clap::{Arg, ArgMatches, Command as ClapCommand};
91/// use std::process::ExitCode;
92/// use std::sync::Arc;
93///
94/// struct GreetCommand;
95///
96/// impl Command for GreetCommand {
97/// fn command() -> ClapCommand {
98/// ClapCommand::new("greet")
99/// .about("Greets a user")
100/// .arg(Arg::new("name")
101/// .help("Name to greet")
102/// .required(true))
103/// }
104///
105/// async fn main(_app: Arc<App>, matches: ArgMatches) -> ExitCode {
106/// let name = matches.get_one::<String>("name").unwrap();
107/// println!("Hello, {}!", name);
108/// ExitCode::SUCCESS
109/// }
110/// }
111/// ```
112pub trait Command: Send + Sync {
113 /// Defines the CLI command structure for this command.
114 ///
115 /// This method should return a `clap::Command` that defines the command name,
116 /// description, arguments, and other CLI options.
117 ///
118 /// # Returns
119 ///
120 /// A `clap::Command` instance describing this command's CLI interface.
121 fn command() -> clap::Command
122 where
123 Self: Sized;
124
125 /// Executes the command with the given application and parsed arguments.
126 ///
127 /// This is the main entry point for command execution. The method receives
128 /// the application container and the parsed command-line arguments.
129 ///
130 /// # Arguments
131 ///
132 /// * `app` - Shared reference to the application container
133 /// * `matches` - Parsed command-line arguments for this command
134 ///
135 /// # Returns
136 ///
137 /// Returns an `ExitCode` indicating the command's execution result.
138 fn main(
139 app: Arc<App>,
140 matches: ArgMatches,
141 ) -> impl std::future::Future<Output = ExitCode> + Send {
142 let _ = (app, matches);
143 async move { ExitCode::FAILURE }
144 }
145}
146
147#[async_trait]
148trait DynCommand: Send + Sync {
149 fn command(&self) -> clap::Command;
150
151 async fn main(&self, app: Arc<App>, matches: ArgMatches) -> ExitCode;
152}
153
154#[async_trait]
155impl<T> DynCommand for T
156where
157 T: Command,
158{
159 fn command(&self) -> clap::Command {
160 T::command()
161 }
162
163 async fn main(&self, app: Arc<App>, matches: ArgMatches) -> ExitCode {
164 T::main(app, matches).await
165 }
166}
167
168/// Registry for managing all commands in the application.
169///
170/// The `CommandRegistry` stores all registered commands and provides functionality
171/// for building the CLI interface and executing commands. It's automatically
172/// managed by the application builder when commands are registered.
173///
174/// # Examples
175///
176/// ```rust
177/// use diode_base::{CommandRegistry, Command, AddCommandExt};
178/// use diode::App;
179///
180/// struct MyCommand;
181/// impl Command for MyCommand {
182/// fn command() -> clap::Command { clap::Command::new("my-cmd") }
183/// }
184///
185/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
186/// let app = App::builder()
187/// .add_command::<MyCommand>()
188/// .build()
189/// .await?;
190/// # Ok(())
191/// # }
192/// ```
193#[derive(Default)]
194#[doc(hidden)]
195pub struct CommandRegistry {
196 commands: HashMap<TypeId, Box<dyn DynCommand>>,
197}
198
199impl CommandRegistry {
200 /// Registers a command type with the registry.
201 ///
202 /// # Type Parameters
203 ///
204 /// * `T` - The command type to register. Must implement `Command + 'static`.
205 pub fn add_command<T>(&mut self)
206 where
207 T: Command + 'static,
208 {
209 let type_id = TypeId::of::<T>();
210 self.commands
211 .insert(type_id, Box::new(CommandWrapper::<T>(PhantomData)));
212 }
213
214 /// Checks if a command type has been registered.
215 ///
216 /// # Type Parameters
217 ///
218 /// * `T` - The command type to check for.
219 ///
220 /// # Returns
221 ///
222 /// Returns `true` if the command is registered, `false` otherwise.
223 pub fn has_command<T>(&self) -> bool
224 where
225 T: Command + 'static,
226 {
227 let type_id = TypeId::of::<T>();
228 self.commands.contains_key(&type_id)
229 }
230
231 /// Builds the complete CLI interface with all registered commands.
232 ///
233 /// Creates a `clap::Command` that includes all registered commands as subcommands
234 /// and sets up common CLI options like config file paths.
235 ///
236 /// # Returns
237 ///
238 /// A `clap::Command` configured with all registered subcommands.
239 pub fn build_cli(&self) -> clap::Command {
240 let mut cli = clap::Command::default()
241 .subcommand_required(true)
242 .arg(Arg::new("config").long("config").short('c').required(true))
243 .arg(
244 Arg::new("config-override")
245 .long("config-override")
246 .short('o')
247 .action(ArgAction::Append),
248 );
249 let mut commands = BTreeMap::new();
250 for command in self.commands.values() {
251 let subcmd = command.command();
252 commands.insert(subcmd.get_name().to_owned(), command);
253 cli = cli.subcommand(subcmd);
254 }
255 cli
256 }
257
258 /// Executes the appropriate command based on parsed CLI arguments.
259 ///
260 /// # Arguments
261 ///
262 /// * `app` - Shared reference to the application container
263 /// * `matches` - Parsed command-line arguments including subcommand selection
264 ///
265 /// # Returns
266 ///
267 /// Returns the exit code from the executed command.
268 pub async fn run_main(&self, app: Arc<App>, mut matches: ArgMatches) -> ExitCode {
269 let (name, matches) = matches.remove_subcommand().unwrap();
270 let command = self
271 .commands
272 .values()
273 .find(|v| v.command().get_name() == name)
274 .unwrap();
275 command.main(app, matches).await
276 }
277
278 /// Returns the number of registered commands.
279 ///
280 /// # Returns
281 ///
282 /// The count of registered commands in this registry.
283 pub fn len(&self) -> usize {
284 self.commands.len()
285 }
286
287 /// Checks if the registry has no registered commands.
288 ///
289 /// # Returns
290 ///
291 /// Returns `true` if no commands are registered, `false` otherwise.
292 pub fn is_empty(&self) -> bool {
293 self.commands.is_empty()
294 }
295}
296
297struct CommandWrapper<T>(PhantomData<T>)
298where
299 T: Command;
300
301impl<T> Command for CommandWrapper<T>
302where
303 T: Command,
304{
305 fn command() -> clap::Command
306 where
307 Self: Sized,
308 {
309 T::command()
310 }
311
312 async fn main(app: Arc<App>, matches: ArgMatches) -> ExitCode {
313 T::main(app, matches).await
314 }
315}
316
317/// Extension trait for `AppBuilder` to add command registration methods.
318///
319/// This trait provides convenient methods for registering commands with the
320/// application builder. Commands registered this way will be available in
321/// the CLI interface when the application is run.
322pub trait AddCommandExt {
323 /// Registers a command with the application builder.
324 ///
325 /// The command will be available as a subcommand in the CLI interface.
326 /// If this is the first command being added, a `CommandRegistry` will
327 /// be automatically created and added to the application.
328 ///
329 /// # Type Parameters
330 ///
331 /// * `T` - The command type to register. Must implement `Command + 'static`.
332 ///
333 /// # Returns
334 ///
335 /// Returns `&mut Self` for method chaining.
336 ///
337 /// # Examples
338 ///
339 /// ```rust
340 /// use diode::{App, AppBuilder};
341 /// use diode_base::{Command, AddCommandExt};
342 /// use clap::Command as ClapCommand;
343 /// use std::process::ExitCode;
344 /// use std::sync::Arc;
345 ///
346 /// struct MyCommand;
347 /// impl Command for MyCommand {
348 /// fn command() -> ClapCommand { ClapCommand::new("my-cmd") }
349 /// }
350 ///
351 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
352 /// let app = App::builder()
353 /// .add_command::<MyCommand>()
354 /// .build()
355 /// .await?;
356 /// # Ok(())
357 /// # }
358 /// ```
359 fn add_command<T>(&mut self) -> &mut Self
360 where
361 T: Command + 'static;
362
363 /// Checks if a command type has been registered.
364 ///
365 /// # Type Parameters
366 ///
367 /// * `T` - The command type to check for.
368 ///
369 /// # Returns
370 ///
371 /// Returns `true` if the command is registered, `false` otherwise.
372 fn has_command<T>(&self) -> bool
373 where
374 T: Command + 'static;
375}
376
377impl AddCommandExt for AppBuilder {
378 fn add_command<T>(&mut self) -> &mut Self
379 where
380 T: Command + 'static,
381 {
382 if !self.has_component::<CommandRegistry>() {
383 self.add_component(CommandRegistry::default());
384 }
385 self.get_component_mut::<CommandRegistry>()
386 .unwrap()
387 .add_command::<T>();
388 self
389 }
390
391 fn has_command<T>(&self) -> bool
392 where
393 T: Command + 'static,
394 {
395 self.get_component_ref::<CommandRegistry>()
396 .is_some_and(|v| v.has_command::<T>())
397 }
398}
399
400/// Extension trait for `AppBuilder` to run the main CLI application.
401///
402/// This trait provides the main entry point for CLI applications, handling
403/// argument parsing, configuration loading, and command execution.
404pub trait RunMainExt {
405 /// Runs the main CLI application.
406 ///
407 /// This method:
408 /// 1. Registers default commands (server, config) if not already present
409 /// 2. Builds the CLI interface from registered commands
410 /// 3. Parses command-line arguments
411 /// 4. Loads and merges configuration files
412 /// 5. Sets up tracing/logging
413 /// 6. Builds the application
414 /// 7. Executes the selected command
415 ///
416 /// # Returns
417 ///
418 /// Returns the exit code from the executed command.
419 ///
420 /// # Examples
421 ///
422 /// ```rust,no_run
423 /// use diode::App;
424 /// use diode_base::RunMainExt;
425 ///
426 /// #[tokio::main]
427 /// async fn main() -> std::process::ExitCode {
428 /// App::builder().run_main().await
429 /// }
430 /// ```
431 fn run_main(&mut self) -> impl std::future::Future<Output = ExitCode> + Send;
432}
433
434impl RunMainExt for AppBuilder {
435 async fn run_main(&mut self) -> ExitCode {
436 if !self.has_command::<ServerCommand>() {
437 self.add_command::<ServerCommand>();
438 }
439 if !self.has_command::<ConfigCommand>() {
440 self.add_command::<ConfigCommand>();
441 }
442 // Setup cli.
443 let command_registry = take(&mut *self.get_component_mut::<CommandRegistry>().unwrap());
444 let cli = command_registry.build_cli();
445 let matches = cli.get_matches();
446 // Setup config.
447 if !self.has_component::<Config>() {
448 let config_path = matches.get_one::<String>("config").unwrap();
449 let mut config = Config::parse_file(config_path).await.unwrap();
450 let config_override_paths = matches
451 .get_many::<String>("config-override")
452 .unwrap_or_default();
453 for path in config_override_paths {
454 let config_override = Config::parse_file(path).await.unwrap();
455 config.merge_from(config_override).unwrap();
456 }
457 self.add_component(config);
458 }
459 // Setup tracing.
460 Tracing::build(&*self).unwrap();
461 // Setup metrics.
462 Metrics::build(&*self).unwrap();
463 // Start app.
464 let app = Arc::new(self.build().await.unwrap());
465 command_registry.run_main(app, matches).await
466 }
467}
468
469/// Built-in server command that runs all registered daemons.
470///
471/// This command starts the application in server mode, running all registered
472/// daemon services until a shutdown signal (Ctrl+C) is received.
473pub struct ServerCommand;
474
475impl Command for ServerCommand {
476 fn command() -> clap::Command
477 where
478 Self: Sized,
479 {
480 clap::Command::new("server")
481 }
482
483 async fn main(app: Arc<App>, _matches: ArgMatches) -> ExitCode {
484 let shutdown = CancellationToken::new();
485 tokio::spawn({
486 let shutdown = shutdown.clone();
487 async move {
488 tokio::signal::ctrl_c()
489 .await
490 .expect("Failed to listen for ctrl_c");
491 shutdown.cancel();
492 }
493 });
494 if let Err(err) = app.run_daemons(shutdown).await {
495 panic!("Failed to run server: {err}");
496 }
497 ExitCode::SUCCESS
498 }
499}
500
501/// Built-in config command that displays the current configuration.
502///
503/// This command prints the current application configuration in JSON format,
504/// useful for debugging configuration loading and merging.
505pub struct ConfigCommand;
506
507impl Command for ConfigCommand {
508 fn command() -> clap::Command
509 where
510 Self: Sized,
511 {
512 clap::Command::new("config")
513 }
514
515 async fn main(app: Arc<App>, _matches: ArgMatches) -> ExitCode {
516 let config = app.get_component_ref::<Config>().unwrap();
517 println!("{}", serde_json::to_string_pretty(&config.configs).unwrap());
518 ExitCode::SUCCESS
519 }
520}