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        ReplInterface::new(
595            self.registry,
596            self.context,
597            self.prompt,
598            Some(self.config),
599            self.help_formatter,
600        )?
601        .run()
602    }
603
604    /// Run with automatic mode detection
605    ///
606    /// Decides between CLI and REPL based on command-line arguments:
607    /// - If arguments provided → CLI mode
608    /// - If no arguments → REPL mode
609    ///
610    /// This is the recommended method for most applications.
611    ///
612    /// # Returns
613    ///
614    /// - `Ok(())` on successful execution
615    /// - `Err(...)` on errors
616    ///
617    /// # Example
618    ///
619    /// ```no_run
620    /// # use dynamic_cli::prelude::*;
621    /// # #[derive(Default)]
622    /// # struct MyContext;
623    /// # impl ExecutionContext for MyContext {
624    /// #     fn as_any(&self) -> &dyn std::any::Any { self }
625    /// #     fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self }
626    /// # }
627    /// # struct MyHandler;
628    /// # impl CommandHandler for MyHandler {
629    /// #     fn execute(&self, _: &mut dyn ExecutionContext, _: &std::collections::HashMap<String, String>) -> dynamic_cli::Result<()> { Ok(()) }
630    /// # }
631    /// # fn main() -> dynamic_cli::Result<()> {
632    /// # let app = CliBuilder::new()
633    /// #     .config_file("commands.yaml")
634    /// #     .context(Box::new(MyContext::default()))
635    /// #     .register_handler("handler", Box::new(MyHandler))
636    /// #     .build()?;
637    /// // Auto-detect: CLI if args, REPL if no args
638    /// app.run()
639    /// # }
640    /// ```
641    pub fn run(self) -> Result<()> {
642        let args: Vec<String> = std::env::args().skip(1).collect();
643
644        if args.is_empty() {
645            // No arguments → REPL mode
646            self.run_repl()
647        } else {
648            // Arguments provided → CLI mode
649            self.run_cli(args)
650        }
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657    use crate::config::schema::{ArgumentDefinition, ArgumentType, CommandDefinition, Metadata};
658
659    // Test context
660    #[derive(Default)]
661    struct TestContext {
662        executed: Vec<String>,
663    }
664
665    impl ExecutionContext for TestContext {
666        fn as_any(&self) -> &dyn std::any::Any {
667            self
668        }
669
670        fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
671            self
672        }
673    }
674
675    // Test handler
676    struct TestHandler {
677        name: String,
678    }
679
680    impl CommandHandler for TestHandler {
681        fn execute(
682            &self,
683            context: &mut dyn ExecutionContext,
684            _args: &HashMap<String, String>,
685        ) -> Result<()> {
686            let ctx =
687                crate::context::downcast_mut::<TestContext>(context).expect("Failed to downcast");
688            ctx.executed.push(self.name.clone());
689            Ok(())
690        }
691    }
692
693    fn create_test_config() -> CommandsConfig {
694        CommandsConfig {
695            metadata: Metadata {
696                version: "1.0.0".to_string(),
697                prompt: "test".to_string(),
698                prompt_suffix: " > ".to_string(),
699            },
700            commands: vec![CommandDefinition {
701                name: "test".to_string(),
702                aliases: vec![],
703                description: "Test command".to_string(),
704                required: true,
705                arguments: vec![],
706                options: vec![],
707                implementation: "test_handler".to_string(),
708            }],
709            global_options: vec![],
710        }
711    }
712
713    #[test]
714    fn test_builder_creation() {
715        let builder = CliBuilder::new();
716        assert!(builder.config.is_none());
717        assert!(builder.context.is_none());
718    }
719
720    #[test]
721    fn test_builder_with_config() {
722        let config = create_test_config();
723        let builder = CliBuilder::new().config(config.clone());
724
725        assert!(builder.config.is_some());
726    }
727
728    #[test]
729    fn test_builder_with_context() {
730        let context = Box::new(TestContext::default());
731        let builder = CliBuilder::new().context(context);
732
733        assert!(builder.context.is_some());
734    }
735
736    #[test]
737    fn test_builder_with_handler() {
738        let handler = Box::new(TestHandler {
739            name: "test".to_string(),
740        });
741
742        let builder = CliBuilder::new().register_handler("test_handler", handler);
743
744        assert_eq!(builder.handlers.len(), 1);
745    }
746
747    #[test]
748    fn test_builder_with_prompt() {
749        let builder = CliBuilder::new().prompt("myapp");
750
751        assert_eq!(builder.prompt, Some("myapp".to_string()));
752    }
753
754    #[test]
755    fn test_builder_build_success() {
756        let config = create_test_config();
757        let context = Box::new(TestContext::default());
758        let handler = Box::new(TestHandler {
759            name: "test".to_string(),
760        });
761
762        let app = CliBuilder::new()
763            .config(config)
764            .context(context)
765            .register_handler("test_handler", handler)
766            .build();
767
768        assert!(app.is_ok());
769    }
770
771    #[test]
772    fn test_builder_build_missing_config() {
773        let context = Box::new(TestContext::default());
774
775        let result = CliBuilder::new().context(context).build();
776
777        assert!(result.is_err());
778        match result.unwrap_err() {
779            DynamicCliError::Config(ConfigError::InvalidSchema { reason, .. }) => {
780                assert!(reason.contains("No configuration provided"));
781            }
782            other => panic!("Expected InvalidSchema error, got: {:?}", other),
783        }
784    }
785
786    #[test]
787    fn test_builder_build_missing_context() {
788        let config = create_test_config();
789
790        let result = CliBuilder::new().config(config).build();
791
792        assert!(result.is_err());
793        match result.unwrap_err() {
794            DynamicCliError::Config(ConfigError::InvalidSchema { reason, .. }) => {
795                assert!(reason.contains("No execution context provided"));
796            }
797            other => panic!("Expected InvalidSchema error, got: {:?}", other),
798        }
799    }
800
801    #[test]
802    fn test_builder_build_missing_required_handler() {
803        let config = create_test_config();
804        let context = Box::new(TestContext::default());
805
806        let result = CliBuilder::new().config(config).context(context).build();
807
808        assert!(result.is_err());
809        match result.unwrap_err() {
810            DynamicCliError::Config(ConfigError::InvalidSchema { reason, .. }) => {
811                assert!(reason.contains("Required command"));
812                assert!(reason.contains("no registered handler"));
813            }
814            other => panic!("Expected InvalidSchema error, got: {:?}", other),
815        }
816    }
817
818    #[test]
819    fn test_builder_chaining() {
820        let config = create_test_config();
821        let context = Box::new(TestContext::default());
822        let handler = Box::new(TestHandler {
823            name: "test".to_string(),
824        });
825
826        // Test that all methods chain correctly
827        let app = CliBuilder::new()
828            .config(config)
829            .context(context)
830            .register_handler("test_handler", handler)
831            .prompt("test")
832            .build();
833
834        assert!(app.is_ok());
835    }
836
837    #[test]
838    fn test_cli_app_run_cli() {
839        let config = create_test_config();
840        let context = Box::new(TestContext::default());
841        let handler = Box::new(TestHandler {
842            name: "test".to_string(),
843        });
844
845        let app = CliBuilder::new()
846            .config(config)
847            .context(context)
848            .register_handler("test_handler", handler)
849            .build()
850            .unwrap();
851
852        // Run with test command
853        let result = app.run_cli(vec!["test".to_string()]);
854        assert!(result.is_ok());
855    }
856
857    #[test]
858    fn test_default_prompt_from_config() {
859        let config = create_test_config();
860        let context = Box::new(TestContext::default());
861        let handler = Box::new(TestHandler {
862            name: "test".to_string(),
863        });
864
865        let app = CliBuilder::new()
866            .config(config)
867            .context(context)
868            .register_handler("test_handler", handler)
869            .build()
870            .unwrap();
871
872        // Prompt should be taken from config
873        assert_eq!(app.prompt, "test");
874    }
875
876    #[test]
877    fn test_override_prompt() {
878        let config = create_test_config();
879        let context = Box::new(TestContext::default());
880        let handler = Box::new(TestHandler {
881            name: "test".to_string(),
882        });
883
884        let app = CliBuilder::new()
885            .config(config)
886            .context(context)
887            .register_handler("test_handler", handler)
888            .prompt("custom")
889            .build()
890            .unwrap();
891
892        // Prompt should be overridden
893        assert_eq!(app.prompt, "custom");
894    }
895
896    #[test]
897    fn test_builder_with_help_formatter() {
898        use crate::help::DefaultHelpFormatter;
899
900        let formatter = Box::new(DefaultHelpFormatter::new());
901        let builder = CliBuilder::new().help_formatter(formatter);
902
903        assert!(builder.help_formatter.is_some());
904    }
905
906    #[test]
907    fn test_run_cli_help_global() {
908        let config = create_test_config();
909        let context = Box::new(TestContext::default());
910        let handler = Box::new(TestHandler {
911            name: "test".to_string(),
912        });
913
914        let app = CliBuilder::new()
915            .config(config)
916            .context(context)
917            .register_handler("test_handler", handler)
918            .build()
919            .unwrap();
920
921        // --help should return Ok(()) without dispatching to any handler.
922        let result = app.run_cli(vec!["--help".to_string()]);
923        assert!(result.is_ok());
924    }
925
926    #[test]
927    fn test_run_cli_help_command() {
928        let config = create_test_config();
929        let context = Box::new(TestContext::default());
930        let handler = Box::new(TestHandler {
931            name: "test".to_string(),
932        });
933
934        let app = CliBuilder::new()
935            .config(config)
936            .context(context)
937            .register_handler("test_handler", handler)
938            .build()
939            .unwrap();
940
941        // --help <command> should return Ok(()) without dispatching.
942        let result = app.run_cli(vec!["--help".to_string(), "test".to_string()]);
943        assert!(result.is_ok());
944    }
945
946    #[test]
947    fn test_run_cli_help_unknown_command_still_ok() {
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        // --help with an unknown command name: formatter handles it gracefully.
962        let result = app.run_cli(vec!["--help".to_string(), "ghost".to_string()]);
963        assert!(result.is_ok());
964    }
965}