Skip to main content

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}