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