Skip to main content

standout_dispatch/
dispatch.rs

1//! Command dispatch logic.
2//!
3//! Core utilities for extracting command paths from clap ArgMatches
4//! and managing the dispatch pipeline.
5
6use clap::ArgMatches;
7
8/// Extracts the command path from ArgMatches by following the subcommand chain.
9///
10/// For example, `myapp db migrate` produces `["db", "migrate"]`.
11pub fn extract_command_path(matches: &ArgMatches) -> Vec<String> {
12    let mut path = Vec::new();
13    let mut current = matches;
14
15    while let Some((name, sub)) = current.subcommand() {
16        // Skip "help" as it's handled separately
17        if name == "help" {
18            break;
19        }
20        path.push(name.to_string());
21        current = sub;
22    }
23
24    path
25}
26
27/// Gets the deepest subcommand matches.
28///
29/// Traverses the subcommand chain and returns the ArgMatches
30/// for the most deeply nested command.
31pub fn get_deepest_matches(matches: &ArgMatches) -> &ArgMatches {
32    let mut current = matches;
33
34    while let Some((name, sub)) = current.subcommand() {
35        if name == "help" {
36            break;
37        }
38        current = sub;
39    }
40
41    current
42}
43
44/// Returns true if the matches contain a subcommand (excluding "help").
45///
46/// Used to detect "naked" CLI invocations where no command was specified.
47pub fn has_subcommand(matches: &ArgMatches) -> bool {
48    matches
49        .subcommand()
50        .map(|(name, _)| name != "help")
51        .unwrap_or(false)
52}
53
54/// Inserts a command name at position 1 (after program name) in the argument list.
55///
56/// Used to implement default command support.
57pub fn insert_default_command<I, S>(args: I, command: &str) -> Vec<String>
58where
59    I: IntoIterator<Item = S>,
60    S: Into<String>,
61{
62    let mut result: Vec<String> = args.into_iter().map(Into::into).collect();
63    if !result.is_empty() {
64        result.insert(1, command.to_string());
65    } else {
66        result.push(command.to_string());
67    }
68    result
69}
70
71/// Converts a command path vector to a dot-separated string.
72///
73/// For example, `["db", "migrate"]` becomes `"db.migrate"`.
74pub fn path_to_string(path: &[String]) -> String {
75    path.join(".")
76}
77
78/// Parses a dot-separated command path string into a vector.
79///
80/// For example, `"db.migrate"` becomes `["db", "migrate"]`.
81pub fn string_to_path(s: &str) -> Vec<String> {
82    if s.is_empty() {
83        Vec::new()
84    } else {
85        s.split('.').map(String::from).collect()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use clap::Command;
93
94    #[test]
95    fn test_extract_command_path() {
96        let cmd =
97            Command::new("app").subcommand(Command::new("config").subcommand(Command::new("get")));
98
99        let matches = cmd.try_get_matches_from(["app", "config", "get"]).unwrap();
100        let path = extract_command_path(&matches);
101
102        assert_eq!(path, vec!["config", "get"]);
103    }
104
105    #[test]
106    fn test_extract_command_path_single() {
107        let cmd = Command::new("app").subcommand(Command::new("list"));
108
109        let matches = cmd.try_get_matches_from(["app", "list"]).unwrap();
110        let path = extract_command_path(&matches);
111
112        assert_eq!(path, vec!["list"]);
113    }
114
115    #[test]
116    fn test_extract_command_path_empty() {
117        let cmd = Command::new("app");
118
119        let matches = cmd.try_get_matches_from(["app"]).unwrap();
120        let path = extract_command_path(&matches);
121
122        assert!(path.is_empty());
123    }
124
125    #[test]
126    fn test_has_subcommand_true() {
127        let cmd = Command::new("app").subcommand(Command::new("list"));
128
129        let matches = cmd.try_get_matches_from(["app", "list"]).unwrap();
130        assert!(has_subcommand(&matches));
131    }
132
133    #[test]
134    fn test_has_subcommand_false() {
135        let cmd = Command::new("app").subcommand(Command::new("list"));
136
137        let matches = cmd.try_get_matches_from(["app"]).unwrap();
138        assert!(!has_subcommand(&matches));
139    }
140
141    #[test]
142    fn test_has_subcommand_help_excluded() {
143        let cmd = Command::new("app")
144            .disable_help_subcommand(true)
145            .subcommand(Command::new("help"));
146
147        let matches = cmd.try_get_matches_from(["app", "help"]).unwrap();
148        assert!(!has_subcommand(&matches));
149    }
150
151    #[test]
152    fn test_insert_default_command() {
153        let args = vec!["myapp", "-v"];
154        let result = insert_default_command(args, "list");
155        assert_eq!(result, vec!["myapp", "list", "-v"]);
156    }
157
158    #[test]
159    fn test_insert_default_command_no_args() {
160        let args = vec!["myapp"];
161        let result = insert_default_command(args, "list");
162        assert_eq!(result, vec!["myapp", "list"]);
163    }
164
165    #[test]
166    fn test_insert_default_command_empty() {
167        let args: Vec<String> = vec![];
168        let result = insert_default_command(args, "list");
169        assert_eq!(result, vec!["list"]);
170    }
171
172    #[test]
173    fn test_path_to_string() {
174        assert_eq!(
175            path_to_string(&["db".into(), "migrate".into()]),
176            "db.migrate"
177        );
178        assert_eq!(path_to_string(&["list".into()]), "list");
179        assert_eq!(path_to_string(&[]), "");
180    }
181
182    #[test]
183    fn test_string_to_path() {
184        assert_eq!(string_to_path("db.migrate"), vec!["db", "migrate"]);
185        assert_eq!(string_to_path("list"), vec!["list"]);
186        assert_eq!(string_to_path(""), Vec::<String>::new());
187    }
188}