1use crate::handlers;
2use crate::parse::WordSet;
3
4pub struct CommandDoc {
5 pub name: &'static str,
6 pub kind: DocKind,
7 pub url: &'static str,
8 pub description: String,
9}
10
11pub enum DocKind {
12 Handler,
13}
14
15impl CommandDoc {
16 pub fn handler(name: &'static str, url: &'static str, description: impl Into<String>) -> Self {
17 let raw = description.into();
18 let description = raw
19 .lines()
20 .map(|line| {
21 if line.is_empty() || line.starts_with("- ") {
22 line.to_string()
23 } else {
24 format!("- {line}")
25 }
26 })
27 .collect::<Vec<_>>()
28 .join("\n");
29 Self { name, kind: DocKind::Handler, url, description }
30 }
31
32 pub fn wordset(name: &'static str, url: &'static str, words: &WordSet) -> Self {
33 Self::handler(name, url, doc(words).build())
34 }
35
36 pub fn wordset_multi(name: &'static str, url: &'static str, words: &WordSet, multi: &[(&str, WordSet)]) -> Self {
37 Self::handler(name, url, doc_multi(words, multi).build())
38 }
39
40
41}
42
43#[derive(Default)]
44pub struct DocBuilder {
45 subcommands: Vec<String>,
46 flags: Vec<String>,
47 sections: Vec<String>,
48}
49
50impl DocBuilder {
51 pub fn new() -> Self {
52 Self::default()
53 }
54
55 pub fn wordset(mut self, words: &WordSet) -> Self {
56 for item in words.iter() {
57 if item.starts_with('-') {
58 self.flags.push(item.to_string());
59 } else {
60 self.subcommands.push(item.to_string());
61 }
62 }
63 self
64 }
65
66 pub fn multi_word(mut self, multi: &[(&str, WordSet)]) -> Self {
67 for (prefix, actions) in multi {
68 for action in actions.iter() {
69 self.subcommands.push(format!("{prefix} {action}"));
70 }
71 }
72 self
73 }
74
75 pub fn triple_word(mut self, triples: &[(&str, &str, WordSet)]) -> Self {
76 for (a, b, actions) in triples {
77 for action in actions.iter() {
78 self.subcommands.push(format!("{a} {b} {action}"));
79 }
80 }
81 self
82 }
83
84 pub fn subcommand(mut self, name: impl Into<String>) -> Self {
85 self.subcommands.push(name.into());
86 self
87 }
88
89 pub fn section(mut self, text: impl Into<String>) -> Self {
90 let s = text.into();
91 if !s.is_empty() {
92 self.sections.push(s);
93 }
94 self
95 }
96
97 pub fn build(self) -> String {
98 let mut lines = Vec::new();
99 if !self.subcommands.is_empty() {
100 let mut subs = self.subcommands;
101 subs.sort();
102 lines.push(format!("- Subcommands: {}", subs.join(", ")));
103 }
104 if !self.flags.is_empty() {
105 lines.push(format!("- Flags: {}", self.flags.join(", ")));
106 }
107 for s in self.sections {
108 if s.starts_with("- ") {
109 lines.push(s);
110 } else {
111 lines.push(format!("- {s}"));
112 }
113 }
114 lines.join("\n")
115 }
116}
117
118pub fn doc(words: &WordSet) -> DocBuilder {
119 DocBuilder::new().wordset(words)
120}
121
122pub fn doc_multi(words: &WordSet, multi: &[(&str, WordSet)]) -> DocBuilder {
123 DocBuilder::new().wordset(words).multi_word(multi)
124}
125
126pub fn wordset_items(words: &WordSet) -> String {
127 let items: Vec<&str> = words.iter().collect();
128 items.join(", ")
129}
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.url, doc.description));
152 }
153
154 out
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn all_commands_have_url() {
163 for doc in all_command_docs() {
164 assert!(!doc.url.is_empty(), "{} has no documentation URL", doc.name);
165 assert!(
166 doc.url.starts_with("https://"),
167 "{} URL must use https: {}",
168 doc.name,
169 doc.url
170 );
171 }
172 }
173
174 #[test]
175 fn builder_two_sections() {
176 let ws = WordSet::new(&["--version", "list", "show"]);
177 assert_eq!(doc(&ws).build(), "- Subcommands: list, show\n- Flags: --version");
178 }
179
180 #[test]
181 fn builder_subcommands_only() {
182 let ws = WordSet::new(&["list", "show"]);
183 assert_eq!(doc(&ws).build(), "- Subcommands: list, show");
184 }
185
186 #[test]
187 fn builder_flags_only() {
188 let ws = WordSet::new(&["--check", "--version"]);
189 assert_eq!(doc(&ws).build(), "- Flags: --check, --version");
190 }
191
192 #[test]
193 fn builder_three_sections() {
194 let ws = WordSet::new(&["--version", "list", "show"]);
195 assert_eq!(
196 doc(&ws).section("Guarded: foo (bar only).").build(),
197 "- Subcommands: list, show\n- Flags: --version\n- Guarded: foo (bar only)."
198 );
199 }
200
201 #[test]
202 fn builder_multi_word_merged() {
203 let ws = WordSet::new(&["--version", "info", "show"]);
204 let multi: &[(&str, WordSet)] =
205 &[("config", WordSet::new(&["get", "list"]))];
206 assert_eq!(
207 doc_multi(&ws, multi).build(),
208 "- Subcommands: config get, config list, info, show\n- Flags: --version"
209 );
210 }
211
212 #[test]
213 fn builder_multi_word_with_extra_section() {
214 let ws = WordSet::new(&["--version", "show"]);
215 let multi: &[(&str, WordSet)] =
216 &[("config", WordSet::new(&["get", "list"]))];
217 assert_eq!(
218 doc_multi(&ws, multi).section("Guarded: foo.").build(),
219 "- Subcommands: config get, config list, show\n- Flags: --version\n- Guarded: foo."
220 );
221 }
222
223 #[test]
224 fn builder_no_flags_with_extra() {
225 let ws = WordSet::new(&["list", "show"]);
226 assert_eq!(
227 doc(&ws).section("Also: foo.").build(),
228 "- Subcommands: list, show\n- Also: foo."
229 );
230 }
231
232 #[test]
233 fn builder_custom_sections_only() {
234 assert_eq!(
235 DocBuilder::new()
236 .section("Read-only: foo.")
237 .section("Always safe: bar.")
238 .section("Guarded: baz.")
239 .build(),
240 "- Read-only: foo.\n- Always safe: bar.\n- Guarded: baz."
241 );
242 }
243
244 #[test]
245 fn builder_triple_word() {
246 let ws = WordSet::new(&["--version", "diff"]);
247 let triples: &[(&str, &str, WordSet)] =
248 &[("git", "remote", WordSet::new(&["list"]))];
249 assert_eq!(
250 doc(&ws).triple_word(triples).build(),
251 "- Subcommands: diff, git remote list\n- Flags: --version"
252 );
253 }
254
255 #[test]
256 fn builder_subcommand_method() {
257 let ws = WordSet::new(&["--version", "list"]);
258 assert_eq!(
259 doc(&ws).subcommand("plugin-list").build(),
260 "- Subcommands: list, plugin-list\n- Flags: --version"
261 );
262 }
263
264}