mithril_doc/
lib.rs

1//! Commands to generate a markdown documentation for the command line.
2
3// LIMITATIONS: Some `Configuration` cannot be generated as precisely as we would like because there is a lack of information in the code.
4// - We don't know which parameter is required or not.
5// - In aggregator, Configuration struct contains all parameters but it's not possible to know which sub command use one parameter.
6
7mod 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/// Information to document a field
21#[derive(Clone, Default, Debug, PartialEq)]
22pub struct FieldDoc {
23    /// Name of the parameter
24    pub parameter: String,
25    /// Long option for the command line
26    pub command_line_long: String,
27    /// Short option for the command line
28    pub command_line_short: String,
29    /// Environment variable
30    pub environment_variable: Option<String>,
31    /// Description of the parameter
32    pub description: String,
33    /// Default value
34    pub default_value: Option<String>,
35    /// Usage example
36    pub example: Option<String>,
37    /// Is a mandatory parameter
38    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/// Information about the struct.
56#[derive(Clone, Default, Debug)]
57pub struct StructDoc {
58    /// Parameter names in the insertion order.
59    parameter_order: Vec<String>,
60
61    /// Parameters description
62    parameters: BTreeMap<String, FieldDoc>,
63}
64
65impl StructDoc {
66    /// Create an empty struct.
67    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    /// Add information about one parameter.
79    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    /// Merge two StructDoc into a third one.
108    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    /// Get a field by its name.
123    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
135/// Extractor for struct without Default trait.
136pub trait Documenter {
137    /// Extract information used to generate documentation.
138    fn extract() -> StructDoc;
139}
140
141/// Extractor for struct with Default trait.
142pub trait DocumenterDefault {
143    /// Extract information used to generate documentation.
144    fn extract() -> StructDoc;
145}
146
147/// Generate documentation
148#[derive(Parser, Debug, PartialEq, Clone)]
149pub struct GenerateDocCommands {
150    /// Generated documentation file
151    #[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    /// Generate the command line documentation.
176    pub fn execute(&self, cmd_to_document: &mut Command) -> Result<(), String> {
177        self.execute_with_configurations(cmd_to_document, HashMap::new())
178    }
179
180    /// Generate the command line documentation with config info.
181    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}