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        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}