dynamic_cli/builder.rs
1//! Fluent builder API for creating CLI/REPL applications
2//!
3//! This module provides a builder pattern for easily constructing
4//! CLI and REPL applications with minimal boilerplate.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use dynamic_cli::prelude::*;
10//! use std::collections::HashMap;
11//!
12//! // Define context
13//! #[derive(Default)]
14//! struct MyContext;
15//!
16//! impl ExecutionContext for MyContext {
17//! fn as_any(&self) -> &dyn std::any::Any { self }
18//! fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
19//! }
20//!
21//! // Define handler
22//! struct HelloCommand;
23//!
24//! impl CommandHandler for HelloCommand {
25//! fn execute(
26//! &self,
27//! _context: &mut dyn ExecutionContext,
28//! args: &HashMap<String, String>,
29//! ) -> dynamic_cli::Result<()> {
30//! println!("Hello!");
31//! Ok(())
32//! }
33//! }
34//!
35//! # fn main() -> dynamic_cli::Result<()> {
36//! // Build and run
37//! CliBuilder::new()
38//! .config_file("commands.yaml")
39//! .context(Box::new(MyContext::default()))
40//! .register_handler("hello_handler", Box::new(HelloCommand))
41//! .build()?
42//! .run()
43//! # }
44//! ```
45
46use crate::config::loader::load_config;
47use crate::config::schema::CommandsConfig;
48use crate::context::ExecutionContext;
49use crate::error::{ConfigError, DynamicCliError, Result};
50use crate::executor::CommandHandler;
51use crate::help::{DefaultHelpFormatter, HelpFormatter};
52use crate::interface::{CliInterface, ReplInterface};
53use crate::plugin::Plugin;
54use crate::registry::CommandRegistry;
55use std::collections::HashMap;
56use std::path::PathBuf;
57
58/// Fluent builder for creating CLI/REPL applications
59///
60/// Provides a chainable API for configuring and building applications.
61/// Automatically loads configuration, registers handlers, and creates
62/// the appropriate interface (CLI or REPL).
63///
64/// # Builder Pattern
65///
66/// The builder follows the standard Rust builder pattern:
67/// - Methods consume `self` and return `Self`
68/// - Final `build()` method consumes the builder and returns the app
69///
70/// # Example
71///
72/// ```no_run
73/// use dynamic_cli::prelude::*;
74///
75/// # #[derive(Default)]
76/// # struct MyContext;
77/// # impl ExecutionContext for MyContext {
78/// # fn as_any(&self) -> &dyn std::any::Any { self }
79/// # fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
80/// # }
81/// # struct MyHandler;
82/// # impl CommandHandler for MyHandler {
83/// # fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
84/// # }
85/// # fn main() -> dynamic_cli::Result<()> {
86/// let app = CliBuilder::new()
87/// .config_file("commands.yaml")
88/// .context(Box::new(MyContext::default()))
89/// .register_handler("my_handler", Box::new(MyHandler))
90/// .prompt("myapp")
91/// .build()?;
92/// # Ok(())
93/// # }
94/// ```
95pub struct CliBuilder {
96 /// Path to configuration file
97 config_path: Option<PathBuf>,
98
99 /// Loaded configuration
100 config: Option<CommandsConfig>,
101
102 /// Execution context
103 context: Option<Box<dyn ExecutionContext>>,
104
105 /// Registered command handlers (name -> handler)
106 handlers: HashMap<String, Box<dyn CommandHandler>>,
107
108 /// Registered plugins, expanded into `handlers` during `build()`
109 plugins: Vec<Box<dyn Plugin>>,
110
111 /// REPL prompt (if None, will use config default or "cli")
112 prompt: Option<String>,
113
114 /// Custom help formatter. None = DefaultHelpFormatter used lazily.
115 help_formatter: Option<Box<dyn HelpFormatter>>,
116}
117
118impl CliBuilder {
119 /// Create a new builder
120 ///
121 /// # Example
122 ///
123 /// ```
124 /// use dynamic_cli::CliBuilder;
125 ///
126 /// let builder = CliBuilder::new();
127 /// ```
128 pub fn new() -> Self {
129 Self {
130 config_path: None,
131 config: None,
132 context: None,
133 handlers: HashMap::new(),
134 plugins: Vec::new(),
135 prompt: None,
136 help_formatter: None,
137 }
138 }
139
140 /// Specify the configuration file
141 ///
142 /// The file will be loaded during `build()`. Supports YAML and JSON formats.
143 ///
144 /// # Arguments
145 ///
146 /// * `path` - Path to the configuration file (`.yaml`, `.yml`, or `.json`)
147 ///
148 /// # Example
149 ///
150 /// ```
151 /// use dynamic_cli::CliBuilder;
152 ///
153 /// let builder = CliBuilder::new()
154 /// .config_file("commands.yaml");
155 /// ```
156 pub fn config_file<P: Into<PathBuf>>(mut self, path: P) -> Self {
157 self.config_path = Some(path.into());
158 self
159 }
160
161 /// Provide a pre-loaded configuration
162 ///
163 /// Use this instead of `config_file()` if you want to load and potentially
164 /// modify the configuration before building.
165 ///
166 /// # Arguments
167 ///
168 /// * `config` - Loaded and validated configuration
169 ///
170 /// # Example
171 ///
172 /// ```no_run
173 /// use dynamic_cli::{CliBuilder, config::loader::load_config};
174 ///
175 /// # fn main() -> dynamic_cli::Result<()> {
176 /// let mut config = load_config("commands.yaml")?;
177 /// // Modify config if needed...
178 ///
179 /// let builder = CliBuilder::new()
180 /// .config(config);
181 /// # Ok(())
182 /// # }
183 /// ```
184 pub fn config(mut self, config: CommandsConfig) -> Self {
185 self.config = Some(config);
186 self
187 }
188
189 /// Set the execution context
190 ///
191 /// The context will be passed to all command handlers and can store
192 /// application state.
193 ///
194 /// # Arguments
195 ///
196 /// * `context` - Boxed execution context implementing `ExecutionContext`
197 ///
198 /// # Example
199 ///
200 /// ```
201 /// use dynamic_cli::prelude::*;
202 ///
203 /// #[derive(Default)]
204 /// struct MyContext {
205 /// count: u32,
206 /// }
207 ///
208 /// impl ExecutionContext for MyContext {
209 /// fn as_any(&self) -> &dyn std::any::Any { self }
210 /// fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
211 /// }
212 ///
213 /// let builder = CliBuilder::new()
214 /// .context(Box::new(MyContext::default()));
215 /// ```
216 pub fn context(mut self, context: Box<dyn ExecutionContext>) -> Self {
217 self.context = Some(context);
218 self
219 }
220
221 /// Register a command handler
222 ///
223 /// Associates a handler with the command's implementation name from the config.
224 /// The name must match the `implementation` field in the command definition.
225 ///
226 /// # Arguments
227 ///
228 /// * `name` - Implementation name from the configuration
229 /// * `handler` - Boxed command handler implementing `CommandHandler`
230 ///
231 /// # Example
232 ///
233 /// ```
234 /// use dynamic_cli::prelude::*;
235 /// use std::collections::HashMap;
236 ///
237 /// struct MyCommand;
238 ///
239 /// impl CommandHandler for MyCommand {
240 /// fn execute(
241 /// &self,
242 /// _ctx: &mut dyn ExecutionContext,
243 /// _args: &HashMap<String, String>,
244 /// ) -> dynamic_cli::Result<()> {
245 /// println!("Executed!");
246 /// Ok(())
247 /// }
248 /// }
249 ///
250 /// let builder = CliBuilder::new()
251 /// .register_handler("my_command", Box::new(MyCommand));
252 /// ```
253 pub fn register_handler(
254 mut self,
255 name: impl Into<String>,
256 handler: Box<dyn CommandHandler>,
257 ) -> Self {
258 self.handlers.insert(name.into(), handler);
259 self
260 }
261
262 /// Register a plugin
263 ///
264 /// A plugin groups related handlers under a single unit. During
265 /// [`build()`][Self::build], each handler declared by the plugin is
266 /// merged into the handler map. Conflicts with already-registered
267 /// handler names (whether from another plugin or from
268 /// [`register_handler`][Self::register_handler]) produce a build-time
269 /// error.
270 ///
271 /// The YAML config remains the sole source of truth for command
272 /// definitions. Plugin handlers are matched by their `implementation`
273 /// name, exactly as with [`register_handler`][Self::register_handler].
274 ///
275 /// # Example
276 ///
277 /// ```
278 /// use dynamic_cli::CliBuilder;
279 /// use dynamic_cli::plugin::SystemPlugin;
280 ///
281 /// let builder = CliBuilder::new()
282 /// .register_plugin(Box::new(SystemPlugin::new()));
283 /// ```
284 pub fn register_plugin(mut self, plugin: Box<dyn Plugin>) -> Self {
285 self.plugins.push(plugin);
286 self
287 }
288
289 /// Load a WASM plugin from disk, map its business functions, and
290 /// register it
291 ///
292 /// Convenience wrapper around [`WasmPlugin::load`][crate::plugin::wasm::WasmPlugin::load],
293 /// [`WasmPlugin::with_function_map`][crate::plugin::wasm::WasmPlugin::with_function_map],
294 /// and [`register_plugin`][Self::register_plugin]. Only available with
295 /// the `wasm-plugins` feature.
296 ///
297 /// `function_map` is mandatory here, unlike the other `WasmPlugin`
298 /// builder methods (`with_format`, `with_metadata`), which have
299 /// reasonable defaults (YAML, file-name-derived metadata). A
300 /// `WasmPlugin` with an empty function map registers zero handlers —
301 /// silently inert — so this wrapper does not offer a path that skips
302 /// mapping. Applications that also need a non-default format or
303 /// explicit metadata should build the `WasmPlugin` directly and pass it
304 /// to [`register_plugin`][Self::register_plugin] instead.
305 ///
306 /// # Errors
307 ///
308 /// Returns an error if the module cannot be loaded or fails mandatory
309 /// export validation. See `WASM_PLUGIN_INTERFACE.md` for the ABI
310 /// contract.
311 ///
312 /// # Example
313 ///
314 /// ```no_run
315 /// use dynamic_cli::CliBuilder;
316 /// use std::path::Path;
317 ///
318 /// # fn main() -> dynamic_cli::Result<()> {
319 /// let builder = CliBuilder::new()
320 /// .register_wasm_plugin(
321 /// Path::new("plugins/greet.wasm"),
322 /// &[("greet_hello", "say_hello")],
323 /// )?;
324 /// # Ok(())
325 /// # }
326 /// ```
327 #[cfg(feature = "wasm-plugins")]
328 pub fn register_wasm_plugin(
329 self,
330 path: &std::path::Path,
331 function_map: &[(&str, &str)],
332 ) -> Result<Self> {
333 let mut plugin = crate::plugin::wasm::WasmPlugin::load(path)?;
334 for &(impl_name, wasm_fn_name) in function_map {
335 plugin = plugin.with_function_map(impl_name, wasm_fn_name);
336 }
337 Ok(self.register_plugin(Box::new(plugin)))
338 }
339
340 /// Set the REPL prompt
341 ///
342 /// Only used in REPL mode. If not specified, uses the prompt from
343 /// the configuration or defaults to "cli".
344 ///
345 /// # Arguments
346 ///
347 /// * `prompt` - Prompt prefix (e.g., "myapp" displays as "myapp > ")
348 ///
349 /// # Example
350 ///
351 /// ```
352 /// use dynamic_cli::CliBuilder;
353 ///
354 /// let builder = CliBuilder::new()
355 /// .prompt("myapp");
356 /// ```
357 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
358 self.prompt = Some(prompt.into());
359 self
360 }
361
362 /// Set a custom help formatter.
363 ///
364 /// By default, [`DefaultHelpFormatter`] is used lazily when `--help` is
365 /// detected. Call this method to supply your own implementation.
366 ///
367 /// The formatter is stored and transferred to [`CliApp`] during `build()`.
368 /// It is instantiated **only** when `--help` is detected in `run_cli()`.
369 ///
370 /// # Arguments
371 ///
372 /// * `formatter` - Boxed implementation of [`HelpFormatter`]
373 ///
374 /// # Example
375 ///
376 /// ```
377 /// use dynamic_cli::CliBuilder;
378 /// use dynamic_cli::help::{HelpFormatter, DefaultHelpFormatter};
379 /// use dynamic_cli::config::schema::CommandsConfig;
380 ///
381 /// struct MyFormatter;
382 ///
383 /// impl HelpFormatter for MyFormatter {
384 /// fn format_app(&self, config: &CommandsConfig) -> String {
385 /// format!("Help for {}", config.metadata.prompt)
386 /// }
387 /// fn format_command(&self, config: &CommandsConfig, command: &str) -> String {
388 /// format!("Help for command '{command}'")
389 /// }
390 /// }
391 ///
392 /// let builder = CliBuilder::new()
393 /// .help_formatter(Box::new(MyFormatter));
394 /// ```
395 pub fn help_formatter(mut self, formatter: Box<dyn HelpFormatter>) -> Self {
396 self.help_formatter = Some(formatter);
397 self
398 }
399
400 /// Build the application
401 ///
402 /// Performs the following steps:
403 /// 1. Load configuration (if `config_file()` was used)
404 /// 2. Validate that a context was provided
405 /// 3. Create the command registry
406 /// 4. Register all command handlers
407 /// 5. Verify that all required commands have handlers
408 /// 6. Create the `CliApp`
409 ///
410 /// # Returns
411 ///
412 /// A configured `CliApp` ready to run
413 ///
414 /// # Errors
415 ///
416 /// - Configuration errors (file not found, invalid format, etc.)
417 /// - Missing context
418 /// - Missing required handlers
419 /// - Registry errors
420 ///
421 /// # Example
422 ///
423 /// ```no_run
424 /// use dynamic_cli::prelude::*;
425 ///
426 /// # #[derive(Default)]
427 /// # struct MyContext;
428 /// # impl ExecutionContext for MyContext {
429 /// # fn as_any(&self) -> &dyn std::any::Any { self }
430 /// # fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
431 /// # }
432 /// # struct MyHandler;
433 /// # impl CommandHandler for MyHandler {
434 /// # fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
435 /// # }
436 /// # fn main() -> dynamic_cli::Result<()> {
437 /// let app = CliBuilder::new()
438 /// .config_file("commands.yaml")
439 /// .context(Box::new(MyContext::default()))
440 /// .register_handler("handler", Box::new(MyHandler))
441 /// .build()?;
442 ///
443 /// // Now app is ready to run
444 /// # Ok(())
445 /// # }
446 /// ```
447 pub fn build(mut self) -> Result<CliApp> {
448 // Load configuration if path was specified
449 let config = if let Some(config) = self.config.take() {
450 config
451 } else if let Some(path) = self.config_path.take() {
452 load_config(path)?
453 } else {
454 return Err(DynamicCliError::Config(ConfigError::InvalidSchema {
455 reason: "No configuration provided. Use config_file() or config()".to_string(),
456 path: None,
457 suggestion: None,
458 }));
459 };
460
461 // Validate context was provided
462 let context = self.context.take().ok_or_else(|| {
463 DynamicCliError::Config(ConfigError::InvalidSchema {
464 reason: "No execution context provided. Use context()".to_string(),
465 path: None,
466 suggestion: None,
467 })
468 })?;
469
470 // Create registry and register commands
471 let mut registry = CommandRegistry::new();
472
473 // Expand plugins into the handler map before processing commands.
474 // Conflicts between plugins (or between a plugin and a directly
475 // registered handler) are detected here and produce a clear error,
476 // before any command resolution begins.
477 for plugin in self.plugins.drain(..) {
478 let plugin_name = plugin.name().to_string();
479 for (impl_name, handler) in plugin.handlers() {
480 if self.handlers.contains_key(&impl_name) {
481 return Err(DynamicCliError::Config(ConfigError::InvalidSchema {
482 reason: format!(
483 "Plugin '{}' tried to register handler '{}' \
484 which is already registered.",
485 plugin_name, impl_name
486 ),
487 path: None,
488 suggestion: Some(format!(
489 "Remove the duplicate call to register_handler(\"{}\") \
490 or rename the implementation in your YAML config.",
491 impl_name
492 )),
493 }));
494 }
495 self.handlers.insert(impl_name, handler);
496 }
497 }
498
499 for command_def in &config.commands {
500 // Find handler for this command
501 let handler = self.handlers.remove(&command_def.implementation);
502
503 // Check if handler is required
504 if command_def.required && handler.is_none() {
505 return Err(DynamicCliError::Config(ConfigError::InvalidSchema {
506 reason: format!(
507 "Required command '{}' has no registered handler (implementation: '{}'). \
508 Use register_handler() to register it.",
509 command_def.name, command_def.implementation
510 ),
511 path: None,
512 suggestion: None,
513 }));
514 }
515
516 // Register command if handler exists
517 if let Some(handler) = handler {
518 registry.register(command_def.clone(), handler)?;
519 }
520 }
521
522 // Determine prompt
523 let prompt = self
524 .prompt
525 .or_else(|| Some(config.metadata.prompt.clone()))
526 .unwrap_or_else(|| "cli".to_string());
527
528 Ok(CliApp {
529 registry,
530 context,
531 prompt,
532 config,
533 help_formatter: self.help_formatter,
534 })
535 }
536}
537
538impl Default for CliBuilder {
539 fn default() -> Self {
540 Self::new()
541 }
542}
543
544/// Built CLI/REPL application
545///
546/// Created by `CliBuilder::build()`. Provides methods to run the application
547/// in different modes:
548/// - `run()` - Auto-detect CLI vs REPL based on arguments
549/// - `run_cli()` - Force CLI mode with specific arguments
550/// - `run_repl()` - Force REPL mode
551///
552/// # Example
553///
554/// ```no_run
555/// use dynamic_cli::prelude::*;
556///
557/// # #[derive(Default)]
558/// # struct MyContext;
559/// # impl ExecutionContext for MyContext {
560/// # fn as_any(&self) -> &dyn std::any::Any { self }
561/// # fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
562/// # }
563/// # struct MyHandler;
564/// # impl CommandHandler for MyHandler {
565/// # fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
566/// # }
567/// # fn main() -> dynamic_cli::Result<()> {
568/// let app = CliBuilder::new()
569/// .config_file("commands.yaml")
570/// .context(Box::new(MyContext::default()))
571/// .register_handler("handler", Box::new(MyHandler))
572/// .build()?;
573///
574/// // Auto-detect mode (CLI if args provided, REPL otherwise)
575/// app.run()
576/// # }
577/// ```
578pub struct CliApp {
579 /// Command registry
580 registry: CommandRegistry,
581
582 /// Execution context
583 context: Box<dyn ExecutionContext>,
584
585 /// REPL prompt
586 prompt: String,
587
588 /// Full configuration - needed by the help formatter
589 config: CommandsConfig,
590
591 /// Custom help formatter, or None to use DefaultHelpFormatter
592 help_formatter: Option<Box<dyn HelpFormatter>>,
593}
594
595impl std::fmt::Debug for CliApp {
596 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
597 f.debug_struct("CliApp")
598 .field("prompt", &self.prompt)
599 .field("registry", &"<CommandRegistry>")
600 .field("context", &"<ExecutionContext>")
601 .field("help_formatter", &"<Option<Box<dyn HelpFormatter>>>")
602 .finish()
603 }
604}
605
606impl CliApp {
607 /// Run in CLI mode with provided arguments
608 ///
609 /// Executes a single command and exits.
610 ///
611 /// # Arguments
612 ///
613 /// * `args` - Command-line arguments (typically from `env::args().skip(1)`)
614 ///
615 /// # Returns
616 ///
617 /// - `Ok(())` on successful execution
618 /// - `Err(...)` on parse, validation, or execution errors
619 ///
620 /// # Example
621 ///
622 /// ```no_run
623 /// # use dynamic_cli::prelude::*;
624 /// # #[derive(Default)]
625 /// # struct MyContext;
626 /// # impl ExecutionContext for MyContext {
627 /// # fn as_any(&self) -> &dyn std::any::Any { self }
628 /// # fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
629 /// # }
630 /// # struct MyHandler;
631 /// # impl CommandHandler for MyHandler {
632 /// # fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
633 /// # }
634 /// # fn main() -> dynamic_cli::Result<()> {
635 /// # let app = CliBuilder::new()
636 /// # .config_file("commands.yaml")
637 /// # .context(Box::new(MyContext::default()))
638 /// # .register_handler("handler", Box::new(MyHandler))
639 /// # .build()?;
640 /// // Run with specific arguments
641 /// app.run_cli(vec!["command".to_string(), "arg1".to_string()])
642 /// # }
643 /// ```
644 pub fn run_cli(self, args: Vec<String>) -> Result<()> {
645 // Intercept --help before command dispatch.
646 // The formatter is instantiated lazily, only when --help is detected.
647 match args.as_slice() {
648 [flag] if flag == "--help" => {
649 let formatter: Box<dyn HelpFormatter> = self
650 .help_formatter
651 .unwrap_or_else(|| Box::new(DefaultHelpFormatter::new()));
652 print!("{}", formatter.format_app(&self.config));
653 return Ok(());
654 }
655 [flag, command] if flag == "--help" => {
656 let formatter: Box<dyn HelpFormatter> = self
657 .help_formatter
658 .unwrap_or_else(|| Box::new(DefaultHelpFormatter::new()));
659 print!("{}", formatter.format_command(&self.config, command));
660 return Ok(());
661 }
662 _ => {}
663 }
664
665 let cli = CliInterface::new(self.registry, self.context);
666 cli.run(args)
667 }
668
669 /// Run in REPL mode
670 ///
671 /// Enters an interactive loop that continues until the user exits.
672 ///
673 /// # Returns
674 ///
675 /// - `Ok(())` when user exits normally
676 /// - `Err(...)` on critical errors (e.g., rustyline initialization failure)
677 ///
678 /// # Example
679 ///
680 /// ```no_run
681 /// # use dynamic_cli::prelude::*;
682 /// # #[derive(Default)]
683 /// # struct MyContext;
684 /// # impl ExecutionContext for MyContext {
685 /// # fn as_any(&self) -> &dyn std::any::Any { self }
686 /// # fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
687 /// # }
688 /// # struct MyHandler;
689 /// # impl CommandHandler for MyHandler {
690 /// # fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
691 /// # }
692 /// # fn main() -> dynamic_cli::Result<()> {
693 /// # let app = CliBuilder::new()
694 /// # .config_file("commands.yaml")
695 /// # .context(Box::new(MyContext::default()))
696 /// # .register_handler("handler", Box::new(MyHandler))
697 /// # .build()?;
698 /// // Start interactive REPL
699 /// app.run_repl()
700 /// # }
701 /// ```
702 pub fn run_repl(self) -> Result<()> {
703 ReplInterface::new(
704 self.registry,
705 self.context,
706 self.prompt,
707 Some(self.config),
708 self.help_formatter,
709 )?
710 .run()
711 }
712
713 /// Run with automatic mode detection
714 ///
715 /// Decides between CLI and REPL based on command-line arguments:
716 /// - If arguments provided → CLI mode
717 /// - If no arguments → REPL mode
718 ///
719 /// This is the recommended method for most applications.
720 ///
721 /// # Returns
722 ///
723 /// - `Ok(())` on successful execution
724 /// - `Err(...)` on errors
725 ///
726 /// # Example
727 ///
728 /// ```no_run
729 /// # use dynamic_cli::prelude::*;
730 /// # #[derive(Default)]
731 /// # struct MyContext;
732 /// # impl ExecutionContext for MyContext {
733 /// # fn as_any(&self) -> &dyn std::any::Any { self }
734 /// # fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
735 /// # }
736 /// # struct MyHandler;
737 /// # impl CommandHandler for MyHandler {
738 /// # fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
739 /// # }
740 /// # fn main() -> dynamic_cli::Result<()> {
741 /// # let app = CliBuilder::new()
742 /// # .config_file("commands.yaml")
743 /// # .context(Box::new(MyContext::default()))
744 /// # .register_handler("handler", Box::new(MyHandler))
745 /// # .build()?;
746 /// // Auto-detect: CLI if args, REPL if no args
747 /// app.run()
748 /// # }
749 /// ```
750 pub fn run(self) -> Result<()> {
751 let args: Vec<String> = std::env::args().skip(1).collect();
752
753 if args.is_empty() {
754 // No arguments → REPL mode
755 self.run_repl()
756 } else {
757 // Arguments provided → CLI mode
758 self.run_cli(args)
759 }
760 }
761}
762
763#[cfg(test)]
764mod tests {
765 use super::*;
766 use crate::config::schema::{ArgumentDefinition, ArgumentType, CommandDefinition, Metadata};
767
768 // Test context
769 #[derive(Default)]
770 struct TestContext {
771 executed: Vec<String>,
772 }
773
774 impl ExecutionContext for TestContext {
775 fn as_any(&self) -> &dyn std::any::Any {
776 self
777 }
778
779 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
780 self
781 }
782 }
783
784 // Test handler
785 struct TestHandler {
786 name: String,
787 }
788
789 impl CommandHandler for TestHandler {
790 fn execute(
791 &self,
792 context: &mut dyn ExecutionContext,
793 _args: &HashMap<String, String>,
794 ) -> Result<()> {
795 let ctx =
796 crate::context::downcast_mut::<TestContext>(context).expect("Failed to downcast");
797 ctx.executed.push(self.name.clone());
798 Ok(())
799 }
800 }
801
802 fn create_test_config() -> CommandsConfig {
803 CommandsConfig {
804 metadata: Metadata {
805 version: "1.0.0".to_string(),
806 prompt: "test".to_string(),
807 prompt_suffix: " > ".to_string(),
808 },
809 commands: vec![CommandDefinition {
810 name: "test".to_string(),
811 aliases: vec![],
812 description: "Test command".to_string(),
813 required: true,
814 arguments: vec![],
815 options: vec![],
816 implementation: "test_handler".to_string(),
817 }],
818 global_options: vec![],
819 }
820 }
821
822 #[test]
823 fn test_builder_creation() {
824 let builder = CliBuilder::new();
825 assert!(builder.config.is_none());
826 assert!(builder.context.is_none());
827 }
828
829 #[test]
830 fn test_builder_with_config() {
831 let config = create_test_config();
832 let builder = CliBuilder::new().config(config.clone());
833
834 assert!(builder.config.is_some());
835 }
836
837 #[test]
838 fn test_builder_with_context() {
839 let context = Box::new(TestContext::default());
840 let builder = CliBuilder::new().context(context);
841
842 assert!(builder.context.is_some());
843 }
844
845 #[test]
846 fn test_builder_with_handler() {
847 let handler = Box::new(TestHandler {
848 name: "test".to_string(),
849 });
850
851 let builder = CliBuilder::new().register_handler("test_handler", handler);
852
853 assert_eq!(builder.handlers.len(), 1);
854 }
855
856 #[test]
857 fn test_builder_with_prompt() {
858 let builder = CliBuilder::new().prompt("myapp");
859
860 assert_eq!(builder.prompt, Some("myapp".to_string()));
861 }
862
863 #[test]
864 fn test_builder_build_success() {
865 let config = create_test_config();
866 let context = Box::new(TestContext::default());
867 let handler = Box::new(TestHandler {
868 name: "test".to_string(),
869 });
870
871 let app = CliBuilder::new()
872 .config(config)
873 .context(context)
874 .register_handler("test_handler", handler)
875 .build();
876
877 assert!(app.is_ok());
878 }
879
880 #[test]
881 fn test_builder_build_missing_config() {
882 let context = Box::new(TestContext::default());
883
884 let result = CliBuilder::new().context(context).build();
885
886 assert!(result.is_err());
887 match result.unwrap_err() {
888 DynamicCliError::Config(ConfigError::InvalidSchema { reason, .. }) => {
889 assert!(reason.contains("No configuration provided"));
890 }
891 other => panic!("Expected InvalidSchema error, got: {:?}", other),
892 }
893 }
894
895 #[test]
896 fn test_builder_build_missing_context() {
897 let config = create_test_config();
898
899 let result = CliBuilder::new().config(config).build();
900
901 assert!(result.is_err());
902 match result.unwrap_err() {
903 DynamicCliError::Config(ConfigError::InvalidSchema { reason, .. }) => {
904 assert!(reason.contains("No execution context provided"));
905 }
906 other => panic!("Expected InvalidSchema error, got: {:?}", other),
907 }
908 }
909
910 #[test]
911 fn test_builder_build_missing_required_handler() {
912 let config = create_test_config();
913 let context = Box::new(TestContext::default());
914
915 let result = CliBuilder::new().config(config).context(context).build();
916
917 assert!(result.is_err());
918 match result.unwrap_err() {
919 DynamicCliError::Config(ConfigError::InvalidSchema { reason, .. }) => {
920 assert!(reason.contains("Required command"));
921 assert!(reason.contains("no registered handler"));
922 }
923 other => panic!("Expected InvalidSchema error, got: {:?}", other),
924 }
925 }
926
927 #[test]
928 fn test_builder_chaining() {
929 let config = create_test_config();
930 let context = Box::new(TestContext::default());
931 let handler = Box::new(TestHandler {
932 name: "test".to_string(),
933 });
934
935 // Test that all methods chain correctly
936 let app = CliBuilder::new()
937 .config(config)
938 .context(context)
939 .register_handler("test_handler", handler)
940 .prompt("test")
941 .build();
942
943 assert!(app.is_ok());
944 }
945
946 #[test]
947 fn test_cli_app_run_cli() {
948 let config = create_test_config();
949 let context = Box::new(TestContext::default());
950 let handler = Box::new(TestHandler {
951 name: "test".to_string(),
952 });
953
954 let app = CliBuilder::new()
955 .config(config)
956 .context(context)
957 .register_handler("test_handler", handler)
958 .build()
959 .unwrap();
960
961 // Run with test command
962 let result = app.run_cli(vec!["test".to_string()]);
963 assert!(result.is_ok());
964 }
965
966 #[test]
967 fn test_default_prompt_from_config() {
968 let config = create_test_config();
969 let context = Box::new(TestContext::default());
970 let handler = Box::new(TestHandler {
971 name: "test".to_string(),
972 });
973
974 let app = CliBuilder::new()
975 .config(config)
976 .context(context)
977 .register_handler("test_handler", handler)
978 .build()
979 .unwrap();
980
981 // Prompt should be taken from config
982 assert_eq!(app.prompt, "test");
983 }
984
985 #[test]
986 fn test_override_prompt() {
987 let config = create_test_config();
988 let context = Box::new(TestContext::default());
989 let handler = Box::new(TestHandler {
990 name: "test".to_string(),
991 });
992
993 let app = CliBuilder::new()
994 .config(config)
995 .context(context)
996 .register_handler("test_handler", handler)
997 .prompt("custom")
998 .build()
999 .unwrap();
1000
1001 // Prompt should be overridden
1002 assert_eq!(app.prompt, "custom");
1003 }
1004
1005 #[test]
1006 fn test_builder_with_help_formatter() {
1007 use crate::help::DefaultHelpFormatter;
1008
1009 let formatter = Box::new(DefaultHelpFormatter::new());
1010 let builder = CliBuilder::new().help_formatter(formatter);
1011
1012 assert!(builder.help_formatter.is_some());
1013 }
1014
1015 #[test]
1016 fn test_run_cli_help_global() {
1017 let config = create_test_config();
1018 let context = Box::new(TestContext::default());
1019 let handler = Box::new(TestHandler {
1020 name: "test".to_string(),
1021 });
1022
1023 let app = CliBuilder::new()
1024 .config(config)
1025 .context(context)
1026 .register_handler("test_handler", handler)
1027 .build()
1028 .unwrap();
1029
1030 // --help should return Ok(()) without dispatching to any handler.
1031 let result = app.run_cli(vec!["--help".to_string()]);
1032 assert!(result.is_ok());
1033 }
1034
1035 #[test]
1036 fn test_run_cli_help_command() {
1037 let config = create_test_config();
1038 let context = Box::new(TestContext::default());
1039 let handler = Box::new(TestHandler {
1040 name: "test".to_string(),
1041 });
1042
1043 let app = CliBuilder::new()
1044 .config(config)
1045 .context(context)
1046 .register_handler("test_handler", handler)
1047 .build()
1048 .unwrap();
1049
1050 // --help <command> should return Ok(()) without dispatching.
1051 let result = app.run_cli(vec!["--help".to_string(), "test".to_string()]);
1052 assert!(result.is_ok());
1053 }
1054
1055 #[test]
1056 fn test_run_cli_help_unknown_command_still_ok() {
1057 let config = create_test_config();
1058 let context = Box::new(TestContext::default());
1059 let handler = Box::new(TestHandler {
1060 name: "test".to_string(),
1061 });
1062
1063 let app = CliBuilder::new()
1064 .config(config)
1065 .context(context)
1066 .register_handler("test_handler", handler)
1067 .build()
1068 .unwrap();
1069
1070 // --help with an unknown command name: formatter handles it gracefully.
1071 let result = app.run_cli(vec!["--help".to_string(), "ghost".to_string()]);
1072 assert!(result.is_ok());
1073 }
1074}