Skip to main content

dynamic_cli/interface/
cli.rs

1//! CLI (Command-Line Interface) implementation
2//!
3//! This module provides a simple CLI interface that parses command-line
4//! arguments, executes the corresponding command, and exits.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use dynamic_cli::interface::CliInterface;
10//! use dynamic_cli::prelude::*;
11//!
12//! # #[derive(Default)]
13//! # struct MyContext;
14//! # impl ExecutionContext for MyContext {
15//! #     fn as_any(&self) -> &dyn std::any::Any { self }
16//! #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
17//! # }
18//! # fn main() -> dynamic_cli::Result<()> {
19//! let registry = CommandRegistry::new();
20//! let context = Box::new(MyContext::default());
21//!
22//! let cli = CliInterface::new(registry, context);
23//! cli.run(std::env::args().skip(1).collect())?;
24//! # Ok(())
25//! # }
26//! ```
27
28use crate::context::ExecutionContext;
29use crate::error::{display_error, DynamicCliError, Result};
30use crate::parser::{CliParser, ParsedArgs};
31use crate::registry::CommandRegistry;
32use std::process;
33
34/// CLI (Command-Line Interface) handler
35///
36/// Provides a simple interface for executing commands from command-line arguments.
37/// The CLI parses arguments, executes the command, and exits.
38///
39/// # Architecture
40///
41/// ```text
42/// Command-line args → CliParser → CommandExecutor → Handler
43///                                       ↓
44///                                  ExecutionContext
45/// ```
46///
47/// # Error Handling
48///
49/// Errors are displayed to stderr with colored formatting (if enabled)
50/// and the process exits with appropriate exit codes:
51/// - `0`: Success
52/// - `1`: Execution error
53/// - `2`: Argument parsing error
54/// - `3`: Other errors
55pub struct CliInterface {
56    /// Command registry containing all available commands
57    registry: CommandRegistry,
58
59    /// Execution context (owned by the interface)
60    context: Box<dyn ExecutionContext>,
61}
62
63impl CliInterface {
64    /// Create a new CLI interface
65    ///
66    /// # Arguments
67    ///
68    /// * `registry` - Command registry with all registered commands
69    /// * `context` - Execution context (will be consumed by the interface)
70    ///
71    /// # Example
72    ///
73    /// ```no_run
74    /// use dynamic_cli::interface::CliInterface;
75    /// use dynamic_cli::prelude::*;
76    ///
77    /// # #[derive(Default)]
78    /// # struct MyContext;
79    /// # impl ExecutionContext for MyContext {
80    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
81    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
82    /// # }
83    /// let registry = CommandRegistry::new();
84    /// let context = Box::new(MyContext::default());
85    ///
86    /// let cli = CliInterface::new(registry, context);
87    /// ```
88    pub fn new(registry: CommandRegistry, context: Box<dyn ExecutionContext>) -> Self {
89        Self { registry, context }
90    }
91
92    /// Run the CLI with provided arguments
93    ///
94    /// Parses the arguments, executes the corresponding command, and handles errors.
95    /// This method consumes `self` as the CLI typically runs once and exits.
96    ///
97    /// # Arguments
98    ///
99    /// * `args` - Command-line arguments (typically from `env::args().skip(1)`)
100    ///
101    /// # Returns
102    ///
103    /// - `Ok(())` on success
104    /// - `Err(DynamicCliError)` on any error (parsing, validation, execution)
105    ///
106    /// # Exit Codes
107    ///
108    /// The caller should handle errors and exit with appropriate codes:
109    /// - Parse errors → exit code 2
110    /// - Execution errors → exit code 1
111    /// - Other errors → exit code 3
112    ///
113    /// # Example
114    ///
115    /// ```no_run
116    /// use dynamic_cli::interface::CliInterface;
117    /// use dynamic_cli::prelude::*;
118    /// use std::process;
119    ///
120    /// # #[derive(Default)]
121    /// # struct MyContext;
122    /// # impl ExecutionContext for MyContext {
123    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
124    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
125    /// # }
126    /// # fn main() {
127    /// let registry = CommandRegistry::new();
128    /// let context = Box::new(MyContext::default());
129    /// let cli = CliInterface::new(registry, context);
130    ///
131    /// if let Err(e) = cli.run(std::env::args().skip(1).collect()) {
132    ///     eprintln!("Error: {}", e);
133    ///     process::exit(1);
134    /// }
135    /// # }
136    /// ```
137    pub fn run(mut self, args: Vec<String>) -> Result<()> {
138        // Handle empty arguments (show help or error)
139        if args.is_empty() {
140            return Err(DynamicCliError::Parse(
141                crate::error::ParseError::InvalidSyntax {
142                    details: "No command specified".to_string(),
143                    hint: Some("Try 'help' to see available commands".to_string()),
144                },
145            ));
146        }
147
148        // First argument is the command name
149        let command_name = &args[0];
150
151        // Resolve command name (handles aliases)
152        let resolved_name = self.registry.resolve_name(command_name).ok_or_else(|| {
153            crate::error::ParseError::unknown_command_with_suggestions(
154                command_name,
155                &self
156                    .registry
157                    .list_commands()
158                    .iter()
159                    .map(|cmd| cmd.name.clone())
160                    .collect::<Vec<_>>(),
161            )
162        })?;
163
164        // Get command definition
165        let definition = self.registry.get_definition(resolved_name).ok_or_else(|| {
166            DynamicCliError::Registry(crate::error::RegistryError::missing_handler(resolved_name))
167        })?;
168
169        // Parse arguments using CLI parser (DD-024/#39: typed to preserve
170        // repeatable-option occurrences; ParsedArgs is the shape every
171        // handler now receives).
172        let parser = CliParser::new(definition);
173        let parsed_args = ParsedArgs::new(parser.parse_typed(&args[1..])?);
174
175        // Get handler and execute command. Sync is tried first (unchanged
176        // behaviour); if no sync handler matches, fall through to the async
177        // path (DD-022) and drive it via `block_on`. Safe here because
178        // `run()` is a strictly sequential, one-shot dispatch — there is no
179        // other async task waiting behind it that `block_on` could starve.
180        if let Some(handler) = self.registry.get_handler_sync(resolved_name) {
181            handler.execute(&mut *self.context, &parsed_args)?;
182        } else if let Some(handler) = self.registry.get_handler_async(resolved_name) {
183            futures::executor::block_on(handler.execute(&mut *self.context, &parsed_args))?;
184        } else {
185            return Err(DynamicCliError::Execution(
186                crate::error::ExecutionError::handler_not_found(
187                    resolved_name,
188                    &definition.implementation,
189                ),
190            ));
191        }
192
193        Ok(())
194    }
195
196    /// Run the CLI with automatic error handling and exit
197    ///
198    /// This is a convenience method that:
199    /// 1. Runs the CLI with provided arguments
200    /// 2. Handles errors by displaying them to stderr
201    /// 3. Exits the process with appropriate exit code
202    ///
203    /// This method never returns.
204    ///
205    /// # Arguments
206    ///
207    /// * `args` - Command-line arguments
208    ///
209    /// # Example
210    ///
211    /// ```no_run
212    /// use dynamic_cli::interface::CliInterface;
213    /// use dynamic_cli::prelude::*;
214    ///
215    /// # #[derive(Default)]
216    /// # struct MyContext;
217    /// # impl ExecutionContext for MyContext {
218    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
219    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
220    /// # }
221    /// # fn main() {
222    /// let registry = CommandRegistry::new();
223    /// let context = Box::new(MyContext::default());
224    /// let cli = CliInterface::new(registry, context);
225    ///
226    /// // This will handle errors and exit automatically
227    /// cli.run_and_exit(std::env::args().skip(1).collect());
228    /// # }
229    /// ```
230    pub fn run_and_exit(self, args: Vec<String>) -> ! {
231        match self.run(args) {
232            Ok(()) => process::exit(0),
233            Err(e) => {
234                display_error(&e);
235
236                // Exit with appropriate code based on error type
237                let exit_code = match e {
238                    DynamicCliError::Parse(_) => 2,
239                    DynamicCliError::Validation(_) => 2,
240                    DynamicCliError::Execution(_) => 1,
241                    _ => 3,
242                };
243
244                process::exit(exit_code);
245            }
246        }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::config::schema::{ArgumentDefinition, ArgumentType, CommandDefinition};
254
255    // Test context
256    #[derive(Default)]
257    struct TestContext {
258        executed_command: Option<String>,
259    }
260
261    impl ExecutionContext for TestContext {
262        fn as_any(&self) -> &dyn std::any::Any {
263            self
264        }
265
266        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
267            self
268        }
269    }
270
271    // Test handler
272    struct TestHandler {
273        name: String,
274    }
275
276    impl crate::executor::CommandHandler for TestHandler {
277        fn execute(&self, context: &mut dyn ExecutionContext, _args: &ParsedArgs) -> Result<()> {
278            let ctx = crate::context::downcast_mut::<TestContext>(context)
279                .expect("Failed to downcast context");
280            ctx.executed_command = Some(self.name.clone());
281            Ok(())
282        }
283    }
284
285    fn create_test_registry() -> CommandRegistry {
286        let mut registry = CommandRegistry::new();
287
288        // Create a simple command definition
289        let cmd_def = CommandDefinition {
290            name: "test".to_string(),
291            aliases: vec!["t".to_string()],
292            description: "Test command".to_string(),
293            required: false,
294            arguments: vec![],
295            options: vec![],
296            implementation: "test_handler".to_string(),
297        };
298
299        let handler = Box::new(TestHandler {
300            name: "test".to_string(),
301        });
302
303        registry
304            .register_sync(cmd_def, handler)
305            .expect("Failed to register command");
306
307        registry
308    }
309
310    #[test]
311    fn test_cli_interface_creation() {
312        let registry = create_test_registry();
313        let context = Box::new(TestContext::default());
314
315        let _cli = CliInterface::new(registry, context);
316        // If this compiles and runs, creation works
317    }
318
319    #[test]
320    fn test_cli_run_simple_command() {
321        let registry = create_test_registry();
322        let context = Box::new(TestContext::default());
323        let cli = CliInterface::new(registry, context);
324
325        let result = cli.run(vec!["test".to_string()]);
326        assert!(result.is_ok());
327    }
328
329    #[test]
330    fn test_cli_run_with_alias() {
331        let registry = create_test_registry();
332        let context = Box::new(TestContext::default());
333        let cli = CliInterface::new(registry, context);
334
335        let result = cli.run(vec!["t".to_string()]);
336        assert!(result.is_ok());
337    }
338
339    #[test]
340    fn test_cli_empty_args() {
341        let registry = create_test_registry();
342        let context = Box::new(TestContext::default());
343        let cli = CliInterface::new(registry, context);
344
345        let result = cli.run(vec![]);
346        assert!(result.is_err());
347
348        match result.unwrap_err() {
349            DynamicCliError::Parse(crate::error::ParseError::InvalidSyntax { .. }) => {}
350            other => panic!("Expected InvalidSyntax error, got: {:?}", other),
351        }
352    }
353
354    #[test]
355    fn test_cli_unknown_command() {
356        let registry = create_test_registry();
357        let context = Box::new(TestContext::default());
358        let cli = CliInterface::new(registry, context);
359
360        let result = cli.run(vec!["unknown".to_string()]);
361        assert!(result.is_err());
362
363        match result.unwrap_err() {
364            DynamicCliError::Parse(crate::error::ParseError::UnknownCommand { .. }) => {}
365            other => panic!("Expected UnknownCommand error, got: {:?}", other),
366        }
367    }
368
369    #[test]
370    fn test_cli_command_with_args() {
371        let mut registry = CommandRegistry::new();
372
373        // Command with argument
374        let cmd_def = CommandDefinition {
375            name: "greet".to_string(),
376            aliases: vec![],
377            description: "Greet someone".to_string(),
378            required: false,
379            arguments: vec![ArgumentDefinition {
380                name: "name".to_string(),
381                arg_type: ArgumentType::String,
382                required: true,
383                description: "Name to greet".to_string(),
384                validation: vec![],
385                secure: false,
386            }],
387            options: vec![],
388            implementation: "greet_handler".to_string(),
389        };
390
391        struct GreetHandler;
392        impl crate::executor::CommandHandler for GreetHandler {
393            fn execute(
394                &self,
395                _context: &mut dyn ExecutionContext,
396                args: &ParsedArgs,
397            ) -> Result<()> {
398                assert_eq!(args.get_scalar("name"), Some("Alice"));
399                Ok(())
400            }
401        }
402
403        registry
404            .register_sync(cmd_def, Box::new(GreetHandler))
405            .unwrap();
406
407        let context = Box::new(TestContext::default());
408        let cli = CliInterface::new(registry, context);
409
410        let result = cli.run(vec!["greet".to_string(), "Alice".to_string()]);
411        assert!(result.is_ok());
412    }
413}