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