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}