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