Skip to main content

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}