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    AlwaysSafe,
12    Handler,
13}
14
15impl CommandDoc {
16    pub fn handler(name: &'static str, description: impl Into<String>) -> Self {
17        Self { name, kind: DocKind::Handler, description: description.into() }
18    }
19
20    pub fn wordset(name: &'static str, words: &WordSet) -> Self {
21        Self::handler(name, describe_wordset(words))
22    }
23
24    pub fn wordset_multi(name: &'static str, words: &WordSet, multi: &[(&str, WordSet)]) -> Self {
25        Self::handler(name, describe_wordset_multi(words, multi))
26    }
27
28    pub fn flagcheck(name: &'static str, check: &FlagCheck) -> Self {
29        Self::handler(name, describe_flagcheck(check))
30    }
31
32    pub fn always_safe(name: &'static str, description: &str) -> Self {
33        Self { name, kind: DocKind::AlwaysSafe, description: description.into() }
34    }
35}
36
37pub fn describe_wordset(words: &WordSet) -> String {
38    let items: Vec<&str> = words.iter().collect();
39    format!("Allowed: {}.", items.join(", "))
40}
41
42pub fn describe_wordset_multi(words: &WordSet, multi: &[(&str, WordSet)]) -> String {
43    let mut parts = Vec::new();
44    let simple: Vec<&str> = words.iter().collect();
45    if !simple.is_empty() {
46        parts.push(format!("Allowed: {}", simple.join(", ")));
47    }
48    if !multi.is_empty() {
49        let multi_strs: Vec<String> = multi
50            .iter()
51            .map(|(prefix, actions)| {
52                let acts: Vec<&str> = actions.iter().collect();
53                format!("{} {}", prefix, acts.join("/"))
54            })
55            .collect();
56        parts.push(format!("Multi-word: {}", multi_strs.join(", ")));
57    }
58    format!("{}.", parts.join(". "))
59}
60
61pub fn describe_flagcheck(check: &FlagCheck) -> String {
62    let mut parts = Vec::new();
63    let req: Vec<&str> = check.required().iter().collect();
64    if !req.is_empty() {
65        parts.push(format!("Requires: {}", req.join(", ")));
66    }
67    let denied: Vec<&str> = check.denied().iter().collect();
68    if !denied.is_empty() {
69        parts.push(format!("Denied: {}", denied.join(", ")));
70    }
71    format!("{}.", parts.join(". "))
72}
73
74pub fn all_command_docs() -> Vec<CommandDoc> {
75    let mut docs = safe_cmd_docs();
76    docs.extend(handlers::handler_docs());
77    docs.sort_by_key(|d| d.name);
78    docs
79}
80
81pub fn render_markdown(docs: &[CommandDoc]) -> String {
82    let mut out = String::from(
83        "# Supported Commands\n\
84         \n\
85         Auto-generated by `safe-chains --list-commands`.\n\
86         \n\
87         Any command with only `--version` or `--help` as its sole argument is always allowed.\n\
88         \n\
89         ## Unconditionally Safe\n\
90         \n\
91         These commands are allowed with any arguments.\n\
92         \n\
93         | Command | Description |\n\
94         |---------|-------------|\n",
95    );
96
97    for doc in docs.iter().filter(|d| matches!(d.kind, DocKind::AlwaysSafe)) {
98        out.push_str(&format!("| `{}` | {} |\n", doc.name, doc.description));
99    }
100
101    out.push_str("\n## Handled Commands\n\nThese commands are allowed with specific subcommands or flags.\n\n");
102
103    for doc in docs.iter().filter(|d| matches!(d.kind, DocKind::Handler)) {
104        out.push_str(&format!("### `{}`\n\n{}\n\n", doc.name, doc.description));
105    }
106
107    out
108}
109
110fn safe_cmd_docs() -> Vec<CommandDoc> {
111    handlers::SAFE_CMD_ENTRIES
112        .iter()
113        .map(|&(name, description)| CommandDoc::always_safe(name, description))
114        .collect()
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn wordset_description() {
123        let ws = WordSet::new(&["--version", "list", "show"]);
124        assert_eq!(describe_wordset(&ws), "Allowed: --version, list, show.");
125    }
126
127    #[test]
128    fn wordset_multi_description() {
129        let simple = WordSet::new(&["--version", "show"]);
130        let multi: &[(&str, WordSet)] =
131            &[("config", WordSet::new(&["get", "list"]))];
132        assert_eq!(
133            describe_wordset_multi(&simple, multi),
134            "Allowed: --version, show. Multi-word: config get/list."
135        );
136    }
137
138    #[test]
139    fn flagcheck_description() {
140        let fc = FlagCheck::new(&["--check"], &["--force"]);
141        assert_eq!(
142            describe_flagcheck(&fc),
143            "Requires: --check. Denied: --force."
144        );
145    }
146
147    #[test]
148    fn flagcheck_required_only() {
149        let fc = FlagCheck::new(&["--check"], &[]);
150        assert_eq!(describe_flagcheck(&fc), "Requires: --check.");
151    }
152}