1mod extract_clap_info;
8mod markdown_formatter;
9mod test_doc_macro;
10
11use clap::{Command, Parser};
12use std::collections::{BTreeMap, HashMap};
13use std::fs::File;
14use std::io::Write;
15
16pub use mithril_doc_derive::{self, *};
17
18const DEFAULT_OUTPUT_FILE_TEMPLATE: &str = "[PROGRAM NAME]-command-line.md";
19
20#[derive(Clone, Default, Debug, PartialEq)]
22pub struct FieldDoc {
23 pub parameter: String,
25 pub command_line_long: String,
27 pub command_line_short: String,
29 pub environment_variable: Option<String>,
31 pub description: String,
33 pub default_value: Option<String>,
35 pub example: Option<String>,
37 pub is_mandatory: bool,
39}
40
41impl FieldDoc {
42 fn merge_field(&mut self, field_doc: &FieldDoc) {
43 if self.default_value.is_none() {
44 self.default_value.clone_from(&field_doc.default_value);
45 }
46 if self.example.is_none() {
47 self.example.clone_from(&field_doc.example);
48 }
49 if self.environment_variable.is_none() {
50 self.environment_variable.clone_from(&field_doc.environment_variable);
51 }
52 }
53}
54
55#[derive(Clone, Default, Debug)]
57pub struct StructDoc {
58 parameter_order: Vec<String>,
60
61 parameters: BTreeMap<String, FieldDoc>,
63}
64
65impl StructDoc {
66 pub fn new(fields: Vec<FieldDoc>) -> StructDoc {
68 let mut struct_doc = StructDoc {
69 parameter_order: vec![],
70 parameters: BTreeMap::new(),
71 };
72 for field in fields {
73 struct_doc.add_field(field);
74 }
75 struct_doc
76 }
77
78 pub fn add_param(
80 &mut self,
81 name: &str,
82 description: &str,
83 environment_variable: Option<String>,
84 default: Option<String>,
85 example: Option<String>,
86 is_mandatory: bool,
87 ) {
88 let field_doc = FieldDoc {
89 parameter: name.to_string(),
90 command_line_long: "".to_string(),
91 command_line_short: "".to_string(),
92 environment_variable,
93 description: description.to_string(),
94 default_value: default,
95 example,
96 is_mandatory,
97 };
98 self.parameter_order.push(field_doc.parameter.to_string());
99 self.parameters.insert(field_doc.parameter.to_string(), field_doc);
100 }
101
102 fn add_field(&mut self, field_doc: FieldDoc) {
103 self.parameter_order.push(field_doc.parameter.to_string());
104 self.parameters.insert(field_doc.parameter.to_string(), field_doc);
105 }
106
107 pub fn merge_struct_doc(&self, s2: &StructDoc) -> StructDoc {
109 let mut struct_doc_merged =
110 StructDoc::new(self.get_ordered_data().into_iter().cloned().collect());
111 for field_doc in s2.get_ordered_data().into_iter() {
112 let key = field_doc.parameter.clone();
113 if let Some(parameter) = struct_doc_merged.parameters.get_mut(&key) {
114 parameter.merge_field(field_doc);
115 } else {
116 struct_doc_merged.add_field(field_doc.clone());
117 }
118 }
119 struct_doc_merged
120 }
121
122 pub fn get_field(&self, name: &str) -> Option<&FieldDoc> {
124 self.parameters.get(name)
125 }
126
127 pub fn get_ordered_data(&self) -> Vec<&FieldDoc> {
128 self.parameter_order
129 .iter()
130 .map(|parameter| self.parameters.get(parameter).unwrap())
131 .collect()
132 }
133}
134
135pub trait Documenter {
137 fn extract() -> StructDoc;
139}
140
141pub trait DocumenterDefault {
143 fn extract() -> StructDoc;
145}
146
147#[derive(Parser, Debug, PartialEq, Clone)]
149pub struct GenerateDocCommands {
150 #[clap(long, default_value = DEFAULT_OUTPUT_FILE_TEMPLATE)]
152 output: String,
153}
154
155impl GenerateDocCommands {
156 fn save_doc(&self, cmd_name: &str, doc: &str) -> Result<(), String> {
157 let output = if self.output.as_str() == DEFAULT_OUTPUT_FILE_TEMPLATE {
158 format!("{cmd_name}-command-line.md")
159 } else {
160 self.output.clone()
161 };
162
163 match File::create(&output) {
164 Ok(mut buffer) => {
165 if write!(buffer, "\n{doc}").is_err() {
166 return Err(format!("Error writing in {output}"));
167 }
168 println!("Documentation generated in file `{}`", &output);
169 }
170 _ => return Err(format!("Could not create {output}")),
171 };
172 Ok(())
173 }
174
175 pub fn execute(&self, cmd_to_document: &mut Command) -> Result<(), String> {
177 self.execute_with_configurations(cmd_to_document, HashMap::new())
178 }
179
180 pub fn execute_with_configurations(
182 &self,
183 cmd_to_document: &mut Command,
184 configs_info: HashMap<String, StructDoc>,
185 ) -> Result<(), String> {
186 let doc = markdown_formatter::doc_markdown_with_config(cmd_to_document, configs_info);
187 let cmd_name = cmd_to_document.get_name();
188
189 println!(
190 "Please note: the documentation generated is not able to indicate the environment variables used by the commands."
191 );
192 self.save_doc(cmd_name, format!("\n{doc}").as_str())
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn test_get_field_must_return_first_field_by_parameter_name() {
202 let mut struct_doc = StructDoc::default();
203 struct_doc.add_param("A", "Param first A", None, None, None, true);
204 struct_doc.add_param("B", "Param first B", None, None, None, true);
205
206 let retrieved_field = struct_doc.get_field("A").unwrap();
207
208 assert_eq!(retrieved_field.description, "Param first A");
209 }
210
211 #[test]
212 fn test_get_field_must_return_none_if_parameter_does_not_exist() {
213 let mut struct_doc = StructDoc::default();
214 struct_doc.add_param("A", "Param first A", None, None, None, true);
215 struct_doc.add_param("B", "Param first B", None, None, None, true);
216
217 let retrieved_field = struct_doc.get_field("X");
218
219 assert_eq!(retrieved_field, None);
220 }
221
222 #[test]
223 fn test_merge_struct_doc() {
224 let s1 = {
225 let mut s = StructDoc::default();
226 s.add_param(
227 "A",
228 "Param first A",
229 Some("env A".to_string()),
230 Some("default A".to_string()),
231 Some("example A".to_string()),
232 true,
233 );
234 s.add_param("B", "Param first B", None, None, None, true);
235 s.add_param(
236 "C",
237 "Param first C",
238 Some("env C".to_string()),
239 Some("default C".to_string()),
240 Some("example C".to_string()),
241 true,
242 );
243 s.add_param("D", "Param first D", None, None, None, true);
244 s
245 };
246
247 let s2 = {
248 let mut s = StructDoc::default();
249 s.add_param("A", "Param second A", None, None, None, true);
250 s.add_param(
251 "B",
252 "Param second B",
253 Some("env B".to_string()),
254 Some("default B".to_string()),
255 Some("example B".to_string()),
256 true,
257 );
258 s.add_param("E", "Param second E", None, None, None, true);
259 s.add_param(
260 "F",
261 "Param second F",
262 Some("env F".to_string()),
263 Some("default F".to_string()),
264 Some("example F".to_string()),
265 true,
266 );
267 s
268 };
269
270 let result = s1.merge_struct_doc(&s2);
271 let data_map = result.parameters;
272
273 assert_eq!(6, data_map.len());
274 assert_eq!("Param first A", data_map.get("A").unwrap().description);
275 assert_eq!("Param first B", data_map.get("B").unwrap().description);
276 assert_eq!("Param first C", data_map.get("C").unwrap().description);
277 assert_eq!("Param first D", data_map.get("D").unwrap().description);
278 assert_eq!("Param second E", data_map.get("E").unwrap().description);
279 assert_eq!("Param second F", data_map.get("F").unwrap().description);
280
281 assert_eq!(
282 Some("default A".to_string()),
283 data_map.get("A").unwrap().default_value
284 );
285 assert_eq!(
286 Some("default B".to_string()),
287 data_map.get("B").unwrap().default_value
288 );
289 assert_eq!(
290 Some("default C".to_string()),
291 data_map.get("C").unwrap().default_value
292 );
293 assert_eq!(None, data_map.get("D").unwrap().default_value);
294 assert_eq!(None, data_map.get("E").unwrap().default_value);
295 assert_eq!(
296 Some("default F".to_string()),
297 data_map.get("F").unwrap().default_value
298 );
299
300 assert_eq!(
301 Some("example A".to_string()),
302 data_map.get("A").unwrap().example
303 );
304 assert_eq!(
305 Some("example B".to_string()),
306 data_map.get("B").unwrap().example
307 );
308 assert_eq!(
309 Some("example C".to_string()),
310 data_map.get("C").unwrap().example
311 );
312 assert_eq!(None, data_map.get("D").unwrap().example);
313 assert_eq!(None, data_map.get("E").unwrap().example);
314 assert_eq!(
315 Some("example F".to_string()),
316 data_map.get("F").unwrap().example
317 );
318
319 assert_eq!(
320 Some("env A".to_string()),
321 data_map.get("A").unwrap().environment_variable
322 );
323 assert_eq!(
324 Some("env B".to_string()),
325 data_map.get("B").unwrap().environment_variable
326 );
327 assert_eq!(
328 Some("env C".to_string()),
329 data_map.get("C").unwrap().environment_variable
330 );
331 assert_eq!(None, data_map.get("D").unwrap().environment_variable);
332 assert_eq!(None, data_map.get("E").unwrap().environment_variable);
333 assert_eq!(
334 Some("env F".to_string()),
335 data_map.get("F").unwrap().environment_variable
336 );
337 }
338
339 #[test]
340 fn test_merge_struct_doc_should_keep_the_order() {
341 fn build_struct_doc(values: &[&str]) -> StructDoc {
342 let mut struct_doc = StructDoc::default();
343 for value in values.iter() {
344 struct_doc.add_param(value, value, None, None, None, true);
345 }
346
347 assert_eq!(
348 struct_doc
349 .get_ordered_data()
350 .iter()
351 .map(|data| data.parameter.to_string())
352 .collect::<Vec<_>>(),
353 values
354 );
355 struct_doc
356 }
357
358 let values_1 = ["A", "E", "C", "B", "D"];
359 let s1 = build_struct_doc(&values_1);
360
361 let values_2 = ["G", "D", "E", "F", "C"];
362 let s2 = build_struct_doc(&values_2);
363
364 let result = s1.merge_struct_doc(&s2);
365 assert_eq!(
366 result
367 .get_ordered_data()
368 .iter()
369 .map(|data| data.parameter.to_string())
370 .collect::<Vec<_>>(),
371 ["A", "E", "C", "B", "D", "G", "F"]
372 );
373 }
374}