Skip to main content

safe_chains/
docs.rs

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}