1use crate::handlers;
2use crate::parse::WordSet;
3
4pub struct CommandDoc {
5 pub name: &'static str,
6 pub kind: DocKind,
7 pub description: String,
8}
9
10pub enum DocKind {
11 Handler,
12}
13
14impl CommandDoc {
15 pub fn handler(name: &'static str, description: impl Into<String>) -> Self {
16 Self { name, kind: DocKind::Handler, description: description.into() }
17 }
18
19 pub fn wordset(name: &'static str, words: &WordSet) -> Self {
20 Self::handler(name, doc(words).build())
21 }
22
23 pub fn wordset_multi(name: &'static str, words: &WordSet, multi: &[(&str, WordSet)]) -> Self {
24 Self::handler(name, doc_multi(words, multi).build())
25 }
26
27
28}
29
30#[derive(Default)]
31pub struct DocBuilder {
32 subcommands: Vec<String>,
33 flags: Vec<String>,
34 sections: Vec<String>,
35}
36
37impl DocBuilder {
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn wordset(mut self, words: &WordSet) -> Self {
43 for item in words.iter() {
44 if item.starts_with('-') {
45 self.flags.push(item.to_string());
46 } else {
47 self.subcommands.push(item.to_string());
48 }
49 }
50 self
51 }
52
53 pub fn multi_word(mut self, multi: &[(&str, WordSet)]) -> Self {
54 for (prefix, actions) in multi {
55 for action in actions.iter() {
56 self.subcommands.push(format!("{prefix} {action}"));
57 }
58 }
59 self
60 }
61
62 pub fn triple_word(mut self, triples: &[(&str, &str, WordSet)]) -> Self {
63 for (a, b, actions) in triples {
64 for action in actions.iter() {
65 self.subcommands.push(format!("{a} {b} {action}"));
66 }
67 }
68 self
69 }
70
71 pub fn subcommand(mut self, name: impl Into<String>) -> Self {
72 self.subcommands.push(name.into());
73 self
74 }
75
76 pub fn section(mut self, text: impl Into<String>) -> Self {
77 let s = text.into();
78 if !s.is_empty() {
79 self.sections.push(s);
80 }
81 self
82 }
83
84 pub fn build(self) -> String {
85 let mut all_sections = Vec::new();
86 if !self.subcommands.is_empty() {
87 let mut subs = self.subcommands;
88 subs.sort();
89 all_sections.push(format!("Subcommands: {}.", subs.join(", ")));
90 }
91 if !self.flags.is_empty() {
92 all_sections.push(format!("Flags: {}.", self.flags.join(", ")));
93 }
94 all_sections.extend(self.sections);
95 if all_sections.len() > 2 {
96 all_sections.join("\n\n")
97 } else {
98 all_sections.join(" ")
99 }
100 }
101}
102
103pub fn doc(words: &WordSet) -> DocBuilder {
104 DocBuilder::new().wordset(words)
105}
106
107pub fn doc_multi(words: &WordSet, multi: &[(&str, WordSet)]) -> DocBuilder {
108 DocBuilder::new().wordset(words).multi_word(multi)
109}
110
111pub fn wordset_items(words: &WordSet) -> String {
112 let items: Vec<&str> = words.iter().collect();
113 items.join(", ")
114}
115
116
117pub fn all_command_docs() -> Vec<CommandDoc> {
118 let mut docs = handlers::handler_docs();
119 docs.sort_by_key(|d| d.name);
120 docs
121}
122
123pub fn render_markdown(docs: &[CommandDoc]) -> String {
124 let mut out = String::from(
125 "# Supported Commands\n\
126 \n\
127 Auto-generated by `safe-chains --list-commands`.\n\
128 \n\
129 Any command with only `--version` or `--help` as its sole argument is always allowed.\n\
130 \n\
131 ## Handled Commands\n\n\
132 These commands are allowed with specific subcommands or flags.\n\n",
133 );
134
135 for doc in docs {
136 out.push_str(&format!("### `{}`\n\n{}\n\n", doc.name, doc.description));
137 }
138
139 out
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn builder_two_sections_inline() {
148 let ws = WordSet::new(&["--version", "list", "show"]);
149 assert_eq!(doc(&ws).build(), "Subcommands: list, show. Flags: --version.");
150 }
151
152 #[test]
153 fn builder_subcommands_only() {
154 let ws = WordSet::new(&["list", "show"]);
155 assert_eq!(doc(&ws).build(), "Subcommands: list, show.");
156 }
157
158 #[test]
159 fn builder_flags_only() {
160 let ws = WordSet::new(&["--check", "--version"]);
161 assert_eq!(doc(&ws).build(), "Flags: --check, --version.");
162 }
163
164 #[test]
165 fn builder_three_sections_newlines() {
166 let ws = WordSet::new(&["--version", "list", "show"]);
167 assert_eq!(
168 doc(&ws).section("Guarded: foo (bar only).").build(),
169 "Subcommands: list, show.\n\nFlags: --version.\n\nGuarded: foo (bar only)."
170 );
171 }
172
173 #[test]
174 fn builder_multi_word_merged() {
175 let ws = WordSet::new(&["--version", "info", "show"]);
176 let multi: &[(&str, WordSet)] =
177 &[("config", WordSet::new(&["get", "list"]))];
178 assert_eq!(
179 doc_multi(&ws, multi).build(),
180 "Subcommands: config get, config list, info, show. Flags: --version."
181 );
182 }
183
184 #[test]
185 fn builder_multi_word_with_extra_section() {
186 let ws = WordSet::new(&["--version", "show"]);
187 let multi: &[(&str, WordSet)] =
188 &[("config", WordSet::new(&["get", "list"]))];
189 assert_eq!(
190 doc_multi(&ws, multi).section("Guarded: foo.").build(),
191 "Subcommands: config get, config list, show.\n\nFlags: --version.\n\nGuarded: foo."
192 );
193 }
194
195 #[test]
196 fn builder_no_flags_with_extra_stays_inline() {
197 let ws = WordSet::new(&["list", "show"]);
198 assert_eq!(
199 doc(&ws).section("Also: foo.").build(),
200 "Subcommands: list, show. Also: foo."
201 );
202 }
203
204 #[test]
205 fn builder_custom_sections_only() {
206 assert_eq!(
207 DocBuilder::new()
208 .section("Read-only: foo.")
209 .section("Always safe: bar.")
210 .section("Guarded: baz.")
211 .build(),
212 "Read-only: foo.\n\nAlways safe: bar.\n\nGuarded: baz."
213 );
214 }
215
216 #[test]
217 fn builder_triple_word() {
218 let ws = WordSet::new(&["--version", "diff"]);
219 let triples: &[(&str, &str, WordSet)] =
220 &[("git", "remote", WordSet::new(&["list"]))];
221 assert_eq!(
222 doc(&ws).triple_word(triples).build(),
223 "Subcommands: diff, git remote list. Flags: --version."
224 );
225 }
226
227 #[test]
228 fn builder_subcommand_method() {
229 let ws = WordSet::new(&["--version", "list"]);
230 assert_eq!(
231 doc(&ws).subcommand("plugin-list").build(),
232 "Subcommands: list, plugin-list. Flags: --version."
233 );
234 }
235
236}