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