clap_sort/
lib.rs

1//! # clap-sort
2//!
3//! A library to validate that clap subcommands are sorted alphabetically.
4//!
5//! This crate provides functionality to validate that clap `Subcommand` enums
6//! have their variants sorted alphabetically by their CLI names at runtime.
7
8/// Validates that subcommands are sorted alphabetically.
9///
10/// This function takes a clap `Command` and checks that all its subcommands
11/// are sorted alphabetically by name.
12///
13/// # Example
14///
15/// ```rust
16/// use clap::{Command, Subcommand};
17///
18/// #[derive(Subcommand)]
19/// enum Commands {
20///     Add,
21///     Delete,
22///     List,
23/// }
24///
25/// let cmd = clap::Command::new("mycli");
26/// // Add subcommands to cmd...
27/// clap_sort::assert_sorted(&cmd);
28/// ```
29pub fn assert_sorted(cmd: &clap::Command) {
30    let subcommands: Vec<_> = cmd.get_subcommands().map(|s| s.get_name()).collect();
31
32    if subcommands.is_empty() {
33        return;
34    }
35
36    let mut sorted = subcommands.clone();
37    sorted.sort();
38
39    if subcommands != sorted {
40        panic!(
41            "Subcommands are not sorted alphabetically!\nActual order: {:?}\nExpected order: {:?}",
42            subcommands, sorted
43        );
44    }
45}
46
47/// Checks if subcommands are sorted, returning a Result instead of panicking.
48///
49/// # Example
50///
51/// ```rust
52/// use clap::Command;
53///
54/// let cmd = Command::new("mycli");
55/// match clap_sort::is_sorted(&cmd) {
56///     Ok(()) => println!("Commands are sorted!"),
57///     Err(msg) => eprintln!("Error: {}", msg),
58/// }
59/// ```
60pub fn is_sorted(cmd: &clap::Command) -> Result<(), String> {
61    let subcommands: Vec<_> = cmd.get_subcommands().map(|s| s.get_name()).collect();
62
63    if subcommands.is_empty() {
64        return Ok(());
65    }
66
67    let mut sorted = subcommands.clone();
68    sorted.sort();
69
70    if subcommands != sorted {
71        Err(format!(
72            "Subcommands are not sorted alphabetically!\nActual order: {:?}\nExpected order: {:?}",
73            subcommands, sorted
74        ))
75    } else {
76        Ok(())
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use clap::{Command, CommandFactory, Parser, Subcommand};
84
85    #[test]
86    fn test_sorted_subcommands() {
87        let cmd = Command::new("test")
88            .subcommand(Command::new("add"))
89            .subcommand(Command::new("delete"))
90            .subcommand(Command::new("list"));
91
92        assert_sorted(&cmd);
93    }
94
95    #[test]
96    #[should_panic(expected = "Subcommands are not sorted")]
97    fn test_unsorted_subcommands() {
98        let cmd = Command::new("test")
99            .subcommand(Command::new("list"))
100            .subcommand(Command::new("add"))
101            .subcommand(Command::new("delete"));
102
103        assert_sorted(&cmd);
104    }
105
106    #[test]
107    fn test_is_sorted_ok() {
108        let cmd = Command::new("test")
109            .subcommand(Command::new("add"))
110            .subcommand(Command::new("delete"))
111            .subcommand(Command::new("list"));
112
113        assert!(is_sorted(&cmd).is_ok());
114    }
115
116    #[test]
117    fn test_is_sorted_err() {
118        let cmd = Command::new("test")
119            .subcommand(Command::new("list"))
120            .subcommand(Command::new("add"));
121
122        assert!(is_sorted(&cmd).is_err());
123    }
124
125    #[test]
126    fn test_no_subcommands() {
127        let cmd = Command::new("test");
128        assert_sorted(&cmd);
129        assert!(is_sorted(&cmd).is_ok());
130    }
131
132    #[test]
133    fn test_with_derive_sorted() {
134        #[derive(Parser)]
135        struct Cli {
136            #[command(subcommand)]
137            command: Commands,
138        }
139
140        #[derive(Subcommand)]
141        enum Commands {
142            Add,
143            Delete,
144            List,
145        }
146
147        let cmd = Cli::command();
148        assert_sorted(&cmd);
149    }
150
151    #[test]
152    #[should_panic(expected = "Subcommands are not sorted")]
153    fn test_with_derive_unsorted() {
154        #[derive(Parser)]
155        struct Cli {
156            #[command(subcommand)]
157            command: Commands,
158        }
159
160        #[derive(Subcommand)]
161        enum Commands {
162            List,
163            Add,
164            Delete,
165        }
166
167        let cmd = Cli::command();
168        assert_sorted(&cmd);
169    }
170}