Skip to main content

safe_chains/
docs.rs

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}