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
51 .clone_from(&field_doc.environment_variable);
52 }
53 }
54}
55
56#[derive(Clone, Default, Debug)]
58pub struct StructDoc {
59 parameter_order: Vec<String>,
61
62 parameters: BTreeMap<String, FieldDoc>,
64}
65
66impl StructDoc {
67 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 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 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 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
138pub trait Documenter {
140 fn extract() -> StructDoc;
142}
143
144pub trait DocumenterDefault {
146 fn extract() -> StructDoc;
148}
149
150#[derive(Parser, Debug, PartialEq, Clone)]
152pub struct GenerateDocCommands {
153 #[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 pub fn execute(&self, cmd_to_document: &mut Command) -> Result<(), String> {
180 self.execute_with_configurations(cmd_to_document, HashMap::new())
181 }
182
183 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}