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}