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}