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