1use crate::parser::{ParseError, Parser};
32use crate::query::Registry;
33use crate::render::{DefaultRenderer, Renderer};
34use crate::resolver::Resolver;
35
36#[derive(Debug, thiserror::Error)]
38pub enum CliError {
39 #[error(transparent)]
44 Parse(#[from] ParseError),
45 #[error("command `{0}` has no handler registered")]
49 NoHandler(String),
50 #[error("handler error: {0}")]
54 Handler(#[from] Box<dyn std::error::Error + Send + Sync>),
55}
56
57pub struct Cli {
93 registry: Registry,
94 app_name: String,
95 version: Option<String>,
96 middlewares: Vec<Box<dyn crate::middleware::Middleware>>,
97 renderer: Box<dyn Renderer>,
98 query_support: bool,
99}
100
101impl Cli {
102 pub fn new(commands: Vec<crate::model::Command>) -> Self {
109 Self {
110 registry: Registry::new(commands),
111 app_name: String::new(),
112 version: None,
113 middlewares: vec![],
114 renderer: Box::new(DefaultRenderer),
115 query_support: false,
116 }
117 }
118
119 pub fn app_name(mut self, name: impl Into<String>) -> Self {
123 self.app_name = name.into();
124 self
125 }
126
127 pub fn version(mut self, version: impl Into<String>) -> Self {
131 self.version = Some(version.into());
132 self
133 }
134
135 pub fn with_middleware<M: crate::middleware::Middleware + 'static>(mut self, m: M) -> Self {
157 self.middlewares.push(Box::new(m));
158 self
159 }
160
161 pub fn with_renderer<R: Renderer + 'static>(mut self, renderer: R) -> Self {
182 self.renderer = Box::new(renderer);
183 self
184 }
185
186 pub fn with_query_support(mut self) -> Self {
208 self.query_support = true;
209 let query_cmd = crate::model::Command::builder("query")
211 .summary("Query command metadata (agent discovery)")
212 .description(
213 "Structured JSON output for agent discovery. \
214 `query commands` lists all commands; `query <name>` returns metadata for one.",
215 )
216 .example(crate::model::Example::new(
217 "query commands",
218 "List all commands as JSON",
219 ))
220 .example(crate::model::Example::new(
221 "query deploy",
222 "Get metadata for the deploy command",
223 ))
224 .build()
225 .expect("built-in query command should always build");
226 self.registry.push(query_cmd);
227 self
228 }
229
230 pub fn run(&self, args: impl IntoIterator<Item = impl AsRef<str>>) -> Result<(), CliError> {
269 let argv: Vec<String> = args.into_iter().map(|a| a.as_ref().to_owned()).collect();
270 let argv_refs: Vec<&str> = argv.iter().map(String::as_str).collect();
271
272 if self.query_support && argv_refs.first().copied() == Some("query") {
274 return self.handle_query(&argv_refs[1..]);
275 }
276
277 if argv_refs.iter().any(|a| *a == "--help" || *a == "-h") {
279 let remaining: Vec<&str> = argv_refs
281 .iter()
282 .copied()
283 .filter(|a| *a != "--help" && *a != "-h")
284 .collect();
285
286 let help_text = self.resolve_help_text(&remaining);
287 print!("{}", help_text);
288 return Ok(());
289 }
290
291 if argv_refs.iter().any(|a| *a == "--version" || *a == "-V") {
293 match &self.version {
294 Some(v) if !self.app_name.is_empty() => println!("{} {}", self.app_name, v),
295 Some(v) => println!("{}", v),
296 None => println!("(no version set)"),
297 }
298 return Ok(());
299 }
300
301 if argv_refs.is_empty() {
303 print!(
304 "{}",
305 self.renderer
306 .render_subcommand_list(self.registry.commands())
307 );
308 return Ok(());
309 }
310
311 let parser = Parser::new(self.registry.commands());
313 match parser.parse(&argv_refs) {
314 Ok(parsed) => {
315 for mw in &self.middlewares {
317 mw.before_dispatch(&parsed).map_err(CliError::Handler)?;
318 }
319
320 let handler_result = match &parsed.command.handler {
322 Some(handler) => {
323 handler(&parsed).map_err(|e| {
326 let msg = e.to_string();
329 let boxed: Box<dyn std::error::Error + Send + Sync> = msg.into();
330 CliError::Handler(boxed)
331 })
332 }
333 None => Err(CliError::NoHandler(parsed.command.canonical.to_string())),
334 };
335
336 let handler_result_for_mw: Result<(), Box<dyn std::error::Error + Send + Sync>> =
338 match &handler_result {
339 Ok(()) => Ok(()),
340 Err(e) => Err(Box::<dyn std::error::Error + Send + Sync>::from(
341 e.to_string(),
342 )),
343 };
344 for mw in &self.middlewares {
345 mw.after_dispatch(&parsed, &handler_result_for_mw);
346 }
347
348 handler_result
349 }
350 Err(parse_err) => {
351 for mw in &self.middlewares {
353 mw.on_parse_error(&parse_err);
354 }
355
356 eprintln!("error: {}", parse_err);
357 if let crate::parser::ParseError::Resolve(
358 crate::resolver::ResolveError::Unknown {
359 ref suggestions, ..
360 },
361 ) = parse_err
362 {
363 if !suggestions.is_empty() {
364 eprintln!("Did you mean one of: {}", suggestions.join(", "));
365 }
366 }
367 let help_text = self.resolve_help_text(&argv_refs);
369 eprint!("{}", help_text);
370 Err(CliError::Parse(parse_err))
371 }
372 }
373 }
374
375 pub fn run_env_args(&self) -> Result<(), CliError> {
384 self.run(std::env::args().skip(1))
385 }
386
387 pub fn run_and_exit(&self, args: impl IntoIterator<Item = impl AsRef<str>>) -> ! {
414 match self.run(args) {
415 Ok(()) => std::process::exit(0),
416 Err(e) => {
417 eprintln!("error: {}", e);
418 std::process::exit(1);
419 }
420 }
421 }
422
423 pub fn run_env_args_and_exit(&self) -> ! {
425 self.run_and_exit(std::env::args().skip(1))
426 }
427
428 #[cfg(feature = "async")]
432 pub async fn run_async_and_exit(&self, args: impl IntoIterator<Item = impl AsRef<str>>) -> ! {
433 match self.run_async(args).await {
434 Ok(()) => std::process::exit(0),
435 Err(e) => {
436 eprintln!("error: {}", e);
437 std::process::exit(1);
438 }
439 }
440 }
441
442 #[cfg(feature = "async")]
444 pub async fn run_env_args_async_and_exit(&self) -> ! {
445 self.run_async_and_exit(std::env::args().skip(1)).await
446 }
447
448 #[cfg(feature = "async")]
466 pub async fn run_async(
467 &self,
468 args: impl IntoIterator<Item = impl AsRef<str>>,
469 ) -> Result<(), CliError> {
470 let args: Vec<String> = args.into_iter().map(|a| a.as_ref().to_string()).collect();
471 let argv: Vec<&str> = args.iter().map(String::as_str).collect();
472
473 if self.query_support && argv.first().copied() == Some("query") {
475 let refs: Vec<&str> = argv.to_vec();
476 return self.handle_query(&refs[1..]);
477 }
478
479 if argv.iter().any(|a| *a == "--help" || *a == "-h") {
481 let remaining: Vec<&str> = argv
482 .iter()
483 .copied()
484 .filter(|a| *a != "--help" && *a != "-h")
485 .collect();
486 let help_text = self.resolve_help_text(&remaining);
487 print!("{}", help_text);
488 return Ok(());
489 }
490
491 if argv.iter().any(|a| *a == "--version" || *a == "-V") {
493 match &self.version {
494 Some(v) if !self.app_name.is_empty() => println!("{} {}", self.app_name, v),
495 Some(v) => println!("{}", v),
496 None => println!("(no version set)"),
497 }
498 return Ok(());
499 }
500
501 if argv.is_empty() {
503 print!(
504 "{}",
505 self.renderer
506 .render_subcommand_list(self.registry.commands())
507 );
508 return Ok(());
509 }
510
511 let parser = Parser::new(self.registry.commands());
513 match parser.parse(&argv) {
514 Ok(parsed) => {
515 for mw in &self.middlewares {
517 mw.before_dispatch(&parsed).map_err(CliError::Handler)?;
518 }
519
520 let handler_result = if let Some(ref async_handler) = parsed.command.async_handler {
522 async_handler(&parsed).await.map_err(|e| {
523 let msg = e.to_string();
524 let boxed: Box<dyn std::error::Error + Send + Sync> = msg.into();
525 CliError::Handler(boxed)
526 })
527 } else if let Some(ref handler) = parsed.command.handler {
528 handler(&parsed).map_err(|e| {
529 let msg = e.to_string();
530 let boxed: Box<dyn std::error::Error + Send + Sync> = msg.into();
531 CliError::Handler(boxed)
532 })
533 } else {
534 Err(CliError::NoHandler(parsed.command.canonical.clone()))
535 };
536
537 let handler_result_for_mw: Result<(), Box<dyn std::error::Error + Send + Sync>> =
539 match &handler_result {
540 Ok(()) => Ok(()),
541 Err(e) => Err(Box::<dyn std::error::Error + Send + Sync>::from(
542 e.to_string(),
543 )),
544 };
545 for mw in &self.middlewares {
546 mw.after_dispatch(&parsed, &handler_result_for_mw);
547 }
548
549 handler_result
550 }
551 Err(parse_err) => {
552 for mw in &self.middlewares {
554 mw.on_parse_error(&parse_err);
555 }
556
557 eprintln!("error: {}", parse_err);
558 if let crate::parser::ParseError::Resolve(
559 crate::resolver::ResolveError::Unknown {
560 ref suggestions, ..
561 },
562 ) = parse_err
563 {
564 if !suggestions.is_empty() {
565 eprintln!("Did you mean one of: {}", suggestions.join(", "));
566 }
567 }
568 let help_text = self.resolve_help_text(&argv);
569 eprint!("{}", help_text);
570 Err(CliError::Parse(parse_err))
571 }
572 }
573 }
574
575 #[cfg(feature = "async")]
577 pub async fn run_env_args_async(&self) -> Result<(), CliError> {
578 self.run_async(std::env::args().skip(1)).await
579 }
580
581 fn handle_query(&self, args: &[&str]) -> Result<(), CliError> {
584 let args: Vec<&str> = args.iter().copied().filter(|a| *a != "--json").collect();
586 let args = args.as_slice();
587
588 match args.first().copied() {
589 None | Some("commands") => {
591 let json = self.registry.to_json().map_err(|e| {
592 CliError::Handler(Box::<dyn std::error::Error + Send + Sync>::from(
593 e.to_string(),
594 ))
595 })?;
596 println!("{}", json);
597 Ok(())
598 }
599 Some("examples") => {
601 let name = args.get(1).copied().ok_or_else(|| {
602 CliError::Handler(Box::<dyn std::error::Error + Send + Sync>::from(
603 "usage: query examples <command-name>",
604 ))
605 })?;
606 let cmd = self
607 .registry
608 .get_command(name)
609 .or_else(|| {
610 let resolver = crate::resolver::Resolver::new(self.registry.commands());
611 resolver.resolve(name).ok()
612 })
613 .ok_or_else(|| {
614 CliError::Handler(Box::<dyn std::error::Error + Send + Sync>::from(
615 format!("unknown command: `{}`", name),
616 ))
617 })?;
618 let json = serde_json::to_string_pretty(&cmd.examples).map_err(|e| {
619 CliError::Handler(Box::<dyn std::error::Error + Send + Sync>::from(
620 e.to_string(),
621 ))
622 })?;
623 println!("{}", json);
624 Ok(())
625 }
626 Some(name) => {
628 let cmd = self.registry.get_command(name);
630 if let Some(cmd) = cmd {
631 let json = serde_json::to_string_pretty(cmd).map_err(|e| {
632 CliError::Handler(Box::<dyn std::error::Error + Send + Sync>::from(
633 e.to_string(),
634 ))
635 })?;
636 println!("{}", json);
637 return Ok(());
638 }
639
640 let resolver = crate::resolver::Resolver::new(self.registry.commands());
642 match resolver.resolve(name) {
643 Ok(cmd) => {
644 let json = serde_json::to_string_pretty(cmd).map_err(|e| {
645 CliError::Handler(Box::<dyn std::error::Error + Send + Sync>::from(
646 e.to_string(),
647 ))
648 })?;
649 println!("{}", json);
650 Ok(())
651 }
652 Err(crate::resolver::ResolveError::Ambiguous { input, candidates }) => {
653 let json = serde_json::json!({
655 "error": "ambiguous",
656 "input": input,
657 "candidates": candidates,
658 });
659 println!("{}", json);
660 Ok(())
661 }
662 Err(crate::resolver::ResolveError::Unknown { .. }) => Err(CliError::Handler(
663 Box::<dyn std::error::Error + Send + Sync>::from(format!(
664 "unknown command: `{}`",
665 name
666 )),
667 )),
668 }
669 }
670 }
671 }
672
673 fn resolve_help_text(&self, argv: &[&str]) -> String {
677 if argv.is_empty() {
679 return self
680 .renderer
681 .render_subcommand_list(self.registry.commands());
682 }
683
684 let words: Vec<&str> = argv
686 .iter()
687 .copied()
688 .filter(|a| !a.starts_with('-'))
689 .collect();
690
691 if words.is_empty() {
692 return self
693 .renderer
694 .render_subcommand_list(self.registry.commands());
695 }
696
697 let resolver = Resolver::new(self.registry.commands());
699 let top_cmd = match resolver.resolve(words[0]) {
700 Ok(cmd) => cmd,
701 Err(_) => {
702 return self
703 .renderer
704 .render_subcommand_list(self.registry.commands())
705 }
706 };
707
708 let mut current = top_cmd;
710 for word in words.iter().skip(1) {
711 if current.subcommands.is_empty() {
712 break;
713 }
714 let sub_resolver = Resolver::new(¤t.subcommands);
715 match sub_resolver.resolve(word) {
716 Ok(sub) => current = sub,
717 Err(_) => break,
718 }
719 }
720
721 self.renderer.render_help(current)
722 }
723}
724
725#[cfg(test)]
728mod tests {
729 use super::*;
730 use crate::model::Command;
731 use std::sync::{Arc, Mutex};
732
733 fn make_cli_no_handler() -> Cli {
734 let cmd = Command::builder("greet")
735 .summary("Say hello")
736 .build()
737 .unwrap();
738 Cli::new(vec![cmd]).app_name("testapp").version("1.2.3")
739 }
740
741 fn make_cli_with_handler(called: Arc<Mutex<bool>>) -> Cli {
742 let cmd = Command::builder("greet")
743 .summary("Say hello")
744 .handler(Arc::new(move |_parsed| {
745 *called.lock().unwrap() = true;
746 Ok(())
747 }))
748 .build()
749 .unwrap();
750 Cli::new(vec![cmd]).app_name("testapp").version("1.2.3")
751 }
752
753 #[test]
754 fn test_run_empty_args() {
755 let cli = make_cli_no_handler();
756 let result = cli.run(std::iter::empty::<&str>());
757 assert!(result.is_ok(), "empty args should return Ok");
758 }
759
760 #[test]
761 fn test_run_help_flag() {
762 let cli = make_cli_no_handler();
763 let result = cli.run(["--help"]);
764 assert!(result.is_ok(), "--help should return Ok");
765 }
766
767 #[test]
768 fn test_run_help_flag_short() {
769 let cli = make_cli_no_handler();
770 let result = cli.run(["-h"]);
771 assert!(result.is_ok(), "-h should return Ok");
772 }
773
774 #[test]
775 fn test_run_version_flag() {
776 let cli = make_cli_no_handler();
777 let result = cli.run(["--version"]);
778 assert!(result.is_ok(), "--version should return Ok");
779 }
780
781 #[test]
782 fn test_run_version_flag_short() {
783 let cli = make_cli_no_handler();
784 let result = cli.run(["-V"]);
785 assert!(result.is_ok(), "-V should return Ok");
786 }
787
788 #[test]
789 fn test_run_no_handler() {
790 let cli = make_cli_no_handler();
791 let result = cli.run(["greet"]);
792 assert!(
793 matches!(result, Err(CliError::NoHandler(ref name)) if name == "greet"),
794 "expected NoHandler(\"greet\"), got {:?}",
795 result
796 );
797 }
798
799 #[test]
800 fn test_run_with_handler() {
801 let called = Arc::new(Mutex::new(false));
802 let cli = make_cli_with_handler(called.clone());
803 let result = cli.run(["greet"]);
804 assert!(result.is_ok(), "handler should succeed, got {:?}", result);
805 assert!(*called.lock().unwrap(), "handler should have been called");
806 }
807
808 #[test]
809 fn test_run_unknown_command() {
810 let cli = make_cli_no_handler();
811 let result = cli.run(["unknowncmd"]);
812 assert!(
813 matches!(result, Err(CliError::Parse(_))),
814 "unknown command should yield Parse error, got {:?}",
815 result
816 );
817 }
818
819 #[test]
820 fn test_run_handler_error_wrapped() {
821 use std::sync::Arc;
822 let cmd = crate::model::Command::builder("fail")
823 .handler(Arc::new(|_| {
824 Err(Box::<dyn std::error::Error>::from("something went wrong"))
825 }))
826 .build()
827 .unwrap();
828 let cli = super::Cli::new(vec![cmd]);
829 let result = cli.run(["fail"]);
830 assert!(result.is_err());
831 match result {
832 Err(super::CliError::Handler(e)) => {
833 assert!(e.to_string().contains("something went wrong"));
834 }
835 other => panic!("expected CliError::Handler, got {:?}", other),
836 }
837 }
838
839 #[test]
840 fn test_run_command_named_help_dispatches_correctly() {
841 use std::sync::atomic::{AtomicBool, Ordering};
845 use std::sync::Arc;
846 let called = Arc::new(AtomicBool::new(false));
847 let called2 = called.clone();
848 let cmd = crate::model::Command::builder("help")
849 .handler(Arc::new(move |_| {
850 called2.store(true, Ordering::SeqCst);
851 Ok(())
852 }))
853 .build()
854 .unwrap();
855 let cli = super::Cli::new(vec![cmd]);
856 cli.run(["help"]).unwrap();
857 assert!(
858 called.load(Ordering::SeqCst),
859 "handler should have been called"
860 );
861 }
862
863 #[test]
864 fn test_middleware_before_dispatch_called() {
865 use crate::middleware::Middleware;
866 use std::sync::atomic::{AtomicBool, Ordering};
867 use std::sync::Arc;
868
869 struct Flag(Arc<AtomicBool>);
870 impl Middleware for Flag {
871 fn before_dispatch(
872 &self,
873 _: &crate::model::ParsedCommand<'_>,
874 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
875 self.0.store(true, Ordering::SeqCst);
876 Ok(())
877 }
878 }
879
880 let called = Arc::new(AtomicBool::new(false));
881 let handler_called = Arc::new(AtomicBool::new(false));
882 let handler_called2 = handler_called.clone();
883
884 let cmd = crate::model::Command::builder("run")
885 .handler(std::sync::Arc::new(move |_| {
886 handler_called2.store(true, Ordering::SeqCst);
887 Ok(())
888 }))
889 .build()
890 .unwrap();
891
892 let cli = super::Cli::new(vec![cmd]).with_middleware(Flag(called.clone()));
893 cli.run(["run"]).unwrap();
894
895 assert!(called.load(Ordering::SeqCst));
896 assert!(handler_called.load(Ordering::SeqCst));
897 }
898
899 #[test]
900 fn test_middleware_can_abort_dispatch() {
901 use crate::middleware::Middleware;
902 struct Aborter;
903 impl Middleware for Aborter {
904 fn before_dispatch(
905 &self,
906 _: &crate::model::ParsedCommand<'_>,
907 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
908 Err("aborted by middleware".into())
909 }
910 }
911
912 let cmd = crate::model::Command::builder("run")
913 .handler(std::sync::Arc::new(|_| panic!("should not be called")))
914 .build()
915 .unwrap();
916
917 let cli = super::Cli::new(vec![cmd]).with_middleware(Aborter);
918 assert!(cli.run(["run"]).is_err());
919 }
920
921 #[test]
922 fn test_query_commands_outputs_json() {
923 use crate::model::Command;
924 let cli = super::Cli::new(vec![
925 Command::builder("deploy")
926 .summary("Deploy")
927 .build()
928 .unwrap(),
929 Command::builder("status")
930 .summary("Status")
931 .build()
932 .unwrap(),
933 ])
934 .with_query_support();
935
936 assert!(cli.run(["query", "commands"]).is_ok());
939 }
940
941 #[test]
942 fn test_query_named_command_outputs_json() {
943 use crate::model::Command;
944 let cli = super::Cli::new(vec![Command::builder("deploy")
945 .summary("Deploy svc")
946 .build()
947 .unwrap()])
948 .with_query_support();
949
950 assert!(cli.run(["query", "deploy"]).is_ok());
951 }
952
953 #[test]
954 fn test_query_unknown_command_errors() {
955 use crate::model::Command;
956 let cli =
957 super::Cli::new(vec![Command::builder("deploy").build().unwrap()]).with_query_support();
958
959 assert!(cli.run(["query", "nonexistent"]).is_err());
960 }
961
962 #[test]
963 fn test_query_meta_command_appears_in_registry() {
964 use crate::model::Command;
965 let cli =
966 super::Cli::new(vec![Command::builder("run").build().unwrap()]).with_query_support();
967
968 assert!(cli.registry.get_command("query").is_some());
970 }
971
972 #[test]
973 fn test_query_with_json_flag() {
974 use crate::model::Command;
975 let cli = super::Cli::new(vec![Command::builder("deploy")
976 .summary("Deploy")
977 .build()
978 .unwrap()])
979 .with_query_support();
980 assert!(cli.run(["query", "deploy", "--json"]).is_ok());
982 assert!(cli.run(["query", "commands", "--json"]).is_ok());
983 }
984
985 #[test]
986 fn test_query_ambiguous_returns_structured_json() {
987 use crate::model::Command;
988 let cli = super::Cli::new(vec![
990 Command::builder("deploy")
991 .summary("Deploy")
992 .build()
993 .unwrap(),
994 Command::builder("describe")
995 .summary("Describe")
996 .build()
997 .unwrap(),
998 ])
999 .with_query_support();
1000
1001 let result = cli.run(["query", "dep"]);
1004 assert!(
1005 result.is_ok(),
1006 "ambiguous query should return Ok(()) with JSON on stdout, got {:?}",
1007 result
1008 );
1009 }
1010
1011 #[test]
1012 fn test_query_examples_returns_examples() {
1013 use crate::model::{Command, Example};
1014 let cli = super::Cli::new(vec![Command::builder("deploy")
1015 .summary("Deploy svc")
1016 .example(Example::new(
1017 "Deploy to production",
1018 "deploy api --env prod",
1019 ))
1020 .build()
1021 .unwrap()])
1022 .with_query_support();
1023
1024 let result = cli.run(["query", "examples", "deploy"]);
1025 assert!(
1026 result.is_ok(),
1027 "query examples for known command should return Ok(()), got {:?}",
1028 result
1029 );
1030 }
1031
1032 #[test]
1033 fn test_query_examples_unknown_errors() {
1034 use crate::model::Command;
1035 let cli =
1036 super::Cli::new(vec![Command::builder("deploy").build().unwrap()]).with_query_support();
1037
1038 let result = cli.run(["query", "examples", "nonexistent"]);
1039 assert!(
1040 result.is_err(),
1041 "query examples for unknown command should return Err, got {:?}",
1042 result
1043 );
1044 }
1045
1046 #[cfg(feature = "async")]
1049 #[tokio::test]
1050 async fn test_run_async_empty_args() {
1051 let cli = make_cli_no_handler();
1052 let result = cli.run_async(std::iter::empty::<&str>()).await;
1053 assert!(
1054 result.is_ok(),
1055 "empty args should return Ok, got {:?}",
1056 result
1057 );
1058 }
1059
1060 #[cfg(feature = "async")]
1061 #[tokio::test]
1062 async fn test_run_async_help_flag() {
1063 let cli = make_cli_no_handler();
1064 let result = cli.run_async(["--help"]).await;
1065 assert!(result.is_ok(), "--help should return Ok, got {:?}", result);
1066 }
1067
1068 #[cfg(feature = "async")]
1069 #[tokio::test]
1070 async fn test_run_async_version_flag() {
1071 let cli = make_cli_no_handler();
1072 let result = cli.run_async(["--version"]).await;
1073 assert!(
1074 result.is_ok(),
1075 "--version should return Ok, got {:?}",
1076 result
1077 );
1078 }
1079
1080 #[cfg(feature = "async")]
1081 #[tokio::test]
1082 async fn test_run_async_with_handler() {
1083 use std::sync::atomic::{AtomicBool, Ordering};
1084 let called = Arc::new(AtomicBool::new(false));
1085 let called2 = called.clone();
1086 let cmd = Command::builder("greet")
1087 .summary("Say hello")
1088 .handler(Arc::new(move |_parsed| {
1089 called2.store(true, Ordering::SeqCst);
1090 Ok(())
1091 }))
1092 .build()
1093 .unwrap();
1094 let cli = super::Cli::new(vec![cmd])
1095 .app_name("testapp")
1096 .version("1.2.3");
1097 let result = cli.run_async(["greet"]).await;
1098 assert!(result.is_ok(), "handler should succeed, got {:?}", result);
1099 assert!(
1100 called.load(Ordering::SeqCst),
1101 "handler should have been called"
1102 );
1103 }
1104
1105 #[cfg(feature = "async")]
1106 #[tokio::test]
1107 async fn test_run_async_unknown_command() {
1108 let cli = make_cli_no_handler();
1109 let result = cli.run_async(["unknowncmd"]).await;
1110 assert!(
1111 matches!(result, Err(CliError::Parse(_))),
1112 "unknown command should yield Parse error, got {:?}",
1113 result
1114 );
1115 }
1116
1117 #[test]
1118 fn test_version_without_app_name() {
1119 let cmd = Command::builder("greet").build().unwrap();
1120 let cli = super::Cli::new(vec![cmd]).version("2.0.0");
1122 assert!(cli.run(["--version"]).is_ok());
1123 }
1124
1125 #[test]
1126 fn test_version_not_set() {
1127 let cmd = Command::builder("greet").build().unwrap();
1128 let cli = super::Cli::new(vec![cmd]);
1130 assert!(cli.run(["--version"]).is_ok());
1131 }
1132
1133 #[test]
1134 fn test_middleware_after_dispatch_called_on_success() {
1135 use crate::middleware::Middleware;
1136 use std::sync::atomic::{AtomicBool, Ordering};
1137 use std::sync::Arc;
1138
1139 struct AfterFlag(Arc<AtomicBool>);
1140 impl Middleware for AfterFlag {
1141 fn after_dispatch(
1142 &self,
1143 _: &crate::model::ParsedCommand<'_>,
1144 _: &Result<(), Box<dyn std::error::Error + Send + Sync>>,
1145 ) {
1146 self.0.store(true, Ordering::SeqCst);
1147 }
1148 }
1149
1150 let called = Arc::new(AtomicBool::new(false));
1151 let cmd = Command::builder("run")
1152 .handler(Arc::new(|_| Ok(())))
1153 .build()
1154 .unwrap();
1155 let cli = super::Cli::new(vec![cmd]).with_middleware(AfterFlag(called.clone()));
1156 cli.run(["run"]).unwrap();
1157 assert!(called.load(Ordering::SeqCst));
1158 }
1159
1160 #[test]
1161 fn test_middleware_after_dispatch_called_on_error() {
1162 use crate::middleware::Middleware;
1163 use std::sync::atomic::{AtomicBool, Ordering};
1164 use std::sync::Arc;
1165
1166 struct AfterFlag(Arc<AtomicBool>);
1167 impl Middleware for AfterFlag {
1168 fn after_dispatch(
1169 &self,
1170 _: &crate::model::ParsedCommand<'_>,
1171 _: &Result<(), Box<dyn std::error::Error + Send + Sync>>,
1172 ) {
1173 self.0.store(true, Ordering::SeqCst);
1174 }
1175 }
1176
1177 let called = Arc::new(AtomicBool::new(false));
1178 let cmd = Command::builder("run")
1179 .handler(Arc::new(|_| Err("handler error".into())))
1180 .build()
1181 .unwrap();
1182 let cli = super::Cli::new(vec![cmd]).with_middleware(AfterFlag(called.clone()));
1183 let _ = cli.run(["run"]);
1184 assert!(called.load(Ordering::SeqCst));
1185 }
1186
1187 #[test]
1188 fn test_middleware_on_parse_error_called() {
1189 use crate::middleware::Middleware;
1190 use std::sync::atomic::{AtomicBool, Ordering};
1191 use std::sync::Arc;
1192
1193 struct OnErrFlag(Arc<AtomicBool>);
1194 impl Middleware for OnErrFlag {
1195 fn on_parse_error(&self, _: &crate::parser::ParseError) {
1196 self.0.store(true, Ordering::SeqCst);
1197 }
1198 }
1199
1200 let called = Arc::new(AtomicBool::new(false));
1201 let cmd = Command::builder("run").build().unwrap();
1202 let cli = super::Cli::new(vec![cmd]).with_middleware(OnErrFlag(called.clone()));
1203 let _ = cli.run(["unknown_xyz"]);
1204 assert!(called.load(Ordering::SeqCst));
1205 }
1206
1207 #[test]
1208 fn test_unknown_command_with_suggestions() {
1209 let cmd = Command::builder("greet").build().unwrap();
1211 let cli = super::Cli::new(vec![cmd]);
1212 let result = cli.run(["gree"]);
1213 assert!(result.is_err());
1215 }
1216
1217 #[test]
1218 fn test_help_for_subcommand() {
1219 let sub = Command::builder("rollback")
1221 .summary("Roll back")
1222 .build()
1223 .unwrap();
1224 let parent = Command::builder("deploy")
1225 .summary("Deploy")
1226 .subcommand(sub)
1227 .build()
1228 .unwrap();
1229 let cli = super::Cli::new(vec![parent]);
1230 let result = cli.run(["deploy", "rollback", "--help"]);
1231 assert!(result.is_ok());
1232 }
1233
1234 #[test]
1235 fn test_help_with_only_flags() {
1236 let cmd = Command::builder("greet").build().unwrap();
1238 let cli = super::Cli::new(vec![cmd]);
1239 let result = cli.run(["--flag", "--help"]);
1240 assert!(result.is_ok());
1241 }
1242
1243 #[test]
1244 fn test_help_for_unknown_command() {
1245 let cmd = Command::builder("greet").build().unwrap();
1247 let cli = super::Cli::new(vec![cmd]);
1248 let result = cli.run(["unknowncmd", "--help"]);
1249 assert!(result.is_ok());
1250 }
1251
1252 #[test]
1253 fn test_query_with_no_arg_outputs_json() {
1254 use crate::model::Command;
1256 let cli =
1257 super::Cli::new(vec![Command::builder("deploy").build().unwrap()]).with_query_support();
1258 assert!(cli.run(["query"]).is_ok());
1259 }
1260
1261 #[test]
1262 fn test_query_examples_via_resolver() {
1263 use crate::model::{Command, Example};
1265 let cli = super::Cli::new(vec![Command::builder("deploy")
1266 .summary("Deploy")
1267 .example(Example::new("prod", "deploy prod"))
1268 .build()
1269 .unwrap()])
1270 .with_query_support();
1271 let result = cli.run(["query", "examples", "dep"]);
1273 assert!(
1274 result.is_ok(),
1275 "query examples via prefix should succeed, got {:?}",
1276 result
1277 );
1278 }
1279
1280 #[test]
1281 fn test_query_named_command_via_resolver() {
1282 use crate::model::Command;
1284 let cli = super::Cli::new(vec![Command::builder("deploy")
1285 .summary("Deploy")
1286 .build()
1287 .unwrap()])
1288 .with_query_support();
1289 let result = cli.run(["query", "dep"]);
1291 assert!(
1292 result.is_ok(),
1293 "query prefix-resolved name should succeed, got {:?}",
1294 result
1295 );
1296 }
1297
1298 #[test]
1299 fn test_query_examples_no_name_errors() {
1300 use crate::model::Command;
1301 let cli =
1302 super::Cli::new(vec![Command::builder("deploy").build().unwrap()]).with_query_support();
1303 let result = cli.run(["query", "examples"]);
1305 assert!(result.is_err(), "query examples with no name should error");
1306 }
1307}