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}