Skip to main content

rh_codegen/generators/
documentation_generator.rs

1//! Documentation generation utilities for FHIR types
2//!
3//! This module contains utilities for generating documentation comments and descriptions
4//! for FHIR structs, fields, traits, and other code elements.
5
6use crate::fhir_types::{ElementDefinition, StructureDefinition};
7use crate::value_sets::ValueSetManager;
8
9/// Generator for documentation and doc comments
10pub struct DocumentationGenerator;
11
12impl DocumentationGenerator {
13    /// Create a new documentation generator
14    pub fn new() -> Self {
15        Self
16    }
17
18    /// Generate comprehensive documentation for a struct based on StructureDefinition metadata
19    pub fn generate_struct_documentation(structure_def: &StructureDefinition) -> Option<String> {
20        let mut doc_lines = Vec::new();
21
22        // Add title if available, otherwise use name
23        if let Some(title) = &structure_def.title {
24            doc_lines.push(title.clone());
25        } else {
26            doc_lines.push(structure_def.name.clone());
27        }
28
29        // Add description if available
30        if let Some(description) = &structure_def.description {
31            doc_lines.push("".to_string());
32            // Clean up the description by removing carriage returns and other problematic characters
33            let cleaned_description = description.replace('\r', "").replace('\n', " ");
34            doc_lines.push(cleaned_description);
35        }
36
37        // Add source information
38        doc_lines.push("".to_string());
39        doc_lines.push("**Source:**".to_string());
40        doc_lines.push(format!("- URL: {url}", url = structure_def.url));
41
42        if let Some(version) = &structure_def.version {
43            doc_lines.push(format!("- Version: {version}"));
44        }
45
46        doc_lines.push(format!("- Kind: {kind}", kind = structure_def.kind));
47        doc_lines.push(format!(
48            "- Type: {base_type}",
49            base_type = structure_def.base_type
50        ));
51
52        if let Some(base_def) = &structure_def.base_definition {
53            doc_lines.push(format!("- Base Definition: {base_def}"));
54        }
55
56        if doc_lines.is_empty() {
57            None
58        } else {
59            Some(doc_lines.join("\n"))
60        }
61    }
62
63    /// Generate documentation for a field based on ElementDefinition
64    pub fn generate_field_documentation(element: &ElementDefinition) -> Option<String> {
65        let mut doc_parts = Vec::new();
66
67        // Add basic field description
68        if let Some(short) = &element.short {
69            doc_parts.push(short.clone());
70        } else if let Some(definition) = &element.definition {
71            doc_parts.push(definition.clone());
72        }
73
74        // Add binding information for non-required bindings
75        if let Some(binding) = &element.binding {
76            if binding.strength != "required" {
77                // Add binding strength information
78                doc_parts.push(format!(
79                    "Binding: {} ({})",
80                    binding.strength,
81                    binding.description.as_deref().unwrap_or("No description")
82                ));
83
84                // Note: We'll add available values here when we have access to ValueSetManager
85            }
86        }
87
88        if doc_parts.is_empty() {
89            None
90        } else {
91            Some(doc_parts.join("\n\n"))
92        }
93    }
94
95    /// Generate enhanced field documentation with ValueSet information
96    pub fn generate_field_documentation_with_binding(
97        element: &ElementDefinition,
98        value_set_manager: &ValueSetManager,
99    ) -> Option<String> {
100        let mut doc_parts = Vec::new();
101
102        // Add basic field description
103        if let Some(short) = &element.short {
104            doc_parts.push(short.clone());
105        } else if let Some(definition) = &element.definition {
106            doc_parts.push(definition.clone());
107        }
108
109        // Add binding information for non-required bindings
110        if let Some(binding) = &element.binding {
111            if binding.strength != "required" {
112                // Add binding strength information
113                doc_parts.push(format!(
114                    "Binding: {} ({})",
115                    binding.strength,
116                    binding.description.as_deref().unwrap_or("No description")
117                ));
118
119                // Add available values from ValueSet
120                if let Some(value_set_url) = &binding.value_set {
121                    // Parse ValueSet URL to remove version if present
122                    let url = if let Some(version_pos) = value_set_url.find('|') {
123                        &value_set_url[..version_pos]
124                    } else {
125                        value_set_url
126                    };
127
128                    match value_set_manager.get_value_set_codes(url, None) {
129                        Ok(codes) => {
130                            if !codes.is_empty() {
131                                let mut values_doc = String::from("Available values:");
132                                for (code, display) in codes.iter().take(10) {
133                                    // Limit to first 10 to avoid huge docs
134                                    if let Some(display) = display {
135                                        values_doc.push_str(&format!("\n- `{code}`: {display}"));
136                                    } else {
137                                        values_doc.push_str(&format!("\n- `{code}`"));
138                                    }
139                                }
140                                if codes.len() > 10 {
141                                    values_doc.push_str(&format!(
142                                        "\n- ... and {} more values",
143                                        codes.len() - 10
144                                    ));
145                                }
146                                doc_parts.push(values_doc);
147                            }
148                        }
149                        Err(_) => {
150                            // ValueSet not found, just add the URL reference
151                            doc_parts.push(format!("ValueSet: {value_set_url}"));
152                        }
153                    }
154                }
155            }
156        }
157
158        if doc_parts.is_empty() {
159            None
160        } else {
161            Some(doc_parts.join("\n\n"))
162        }
163    }
164
165    /// Generate documentation for a choice type field
166    pub fn generate_choice_field_documentation(
167        element: &ElementDefinition,
168        type_code: &str,
169    ) -> Option<String> {
170        // Create documentation that indicates this is a specific type variant of a choice field
171        let base_doc = if let Some(short) = &element.short {
172            short.clone()
173        } else if let Some(definition) = &element.definition {
174            definition.clone()
175        } else {
176            "Choice type field".to_string()
177        };
178
179        // Add type-specific suffix
180        Some(format!("{base_doc} ({type_code})"))
181    }
182
183    /// Generate enhanced choice field documentation with ValueSet information
184    pub fn generate_choice_field_documentation_with_binding(
185        element: &ElementDefinition,
186        type_code: &str,
187        value_set_manager: &ValueSetManager,
188    ) -> Option<String> {
189        let mut doc_parts = Vec::new();
190
191        // Create base documentation that indicates this is a specific type variant of a choice field
192        let base_doc = if let Some(short) = &element.short {
193            short.clone()
194        } else if let Some(definition) = &element.definition {
195            definition.clone()
196        } else {
197            "Choice type field".to_string()
198        };
199
200        // Add type-specific suffix
201        doc_parts.push(format!("{base_doc} ({type_code})"));
202
203        // Add binding information for code type choice fields with non-required bindings
204        if type_code == "code" {
205            if let Some(binding) = &element.binding {
206                if binding.strength != "required" {
207                    // Add binding strength information
208                    doc_parts.push(format!(
209                        "Binding: {} ({})",
210                        binding.strength,
211                        binding.description.as_deref().unwrap_or("No description")
212                    ));
213
214                    // Add available values from ValueSet
215                    if let Some(value_set_url) = &binding.value_set {
216                        // Parse ValueSet URL to remove version if present
217                        let url = if let Some(version_pos) = value_set_url.find('|') {
218                            &value_set_url[..version_pos]
219                        } else {
220                            value_set_url
221                        };
222
223                        match value_set_manager.get_value_set_codes(url, None) {
224                            Ok(codes) => {
225                                if !codes.is_empty() {
226                                    let mut values_doc = String::from("Available values:");
227                                    for (code, display) in codes.iter().take(10) {
228                                        // Limit to first 10 to avoid huge docs
229                                        if let Some(display) = display {
230                                            values_doc
231                                                .push_str(&format!("\n- `{code}`: {display}"));
232                                        } else {
233                                            values_doc.push_str(&format!("\n- `{code}`"));
234                                        }
235                                    }
236                                    if codes.len() > 10 {
237                                        values_doc.push_str(&format!(
238                                            "\n- ... and {} more values",
239                                            codes.len() - 10
240                                        ));
241                                    }
242                                    doc_parts.push(values_doc);
243                                }
244                            }
245                            Err(_) => {
246                                // ValueSet not found, just add the URL reference
247                                doc_parts.push(format!("ValueSet: {value_set_url}"));
248                            }
249                        }
250                    }
251                }
252            }
253        }
254
255        if doc_parts.is_empty() {
256            None
257        } else {
258            Some(doc_parts.join("\n\n"))
259        }
260    }
261
262    /// Generate documentation for a primitive element struct
263    pub fn generate_primitive_element_documentation(primitive_name: &str) -> String {
264        format!(
265            "Element structure for the '{primitive_name}' primitive type. Contains metadata and extensions."
266        )
267    }
268
269    /// Generate documentation for a nested struct
270    pub fn generate_nested_struct_documentation(
271        parent_struct_name: &str,
272        nested_field_name: &str,
273    ) -> String {
274        format!("{parent_struct_name} nested structure for the '{nested_field_name}' field")
275    }
276
277    /// Generate documentation for a sub-nested struct
278    pub fn generate_sub_nested_struct_documentation(
279        nested_struct_name: &str,
280        sub_nested_field_name: &str,
281    ) -> String {
282        format!("{nested_struct_name} nested structure for the '{sub_nested_field_name}' field")
283    }
284
285    /// Generate documentation for primitive type aliases
286    pub fn generate_primitive_type_alias_documentation(
287        structure_def: &StructureDefinition,
288    ) -> String {
289        if let Some(description) = &structure_def.description {
290            description.clone()
291        } else {
292            format!("FHIR primitive type: {name}", name = structure_def.name)
293        }
294    }
295
296    /// Clean description text by removing problematic characters
297    pub fn clean_description(description: &str) -> String {
298        description.replace('\r', "").replace('\n', " ")
299    }
300
301    /// Generate standard FHIR source information block
302    pub fn generate_source_info_block(structure_def: &StructureDefinition) -> Vec<String> {
303        let mut lines = vec![
304            "".to_string(),
305            "**Source:**".to_string(),
306            format!("- URL: {url}", url = structure_def.url),
307        ];
308
309        if let Some(version) = &structure_def.version {
310            lines.push(format!("- Version: {version}"));
311        }
312
313        lines.push(format!("- Kind: {kind}", kind = structure_def.kind));
314        lines.push(format!(
315            "- Type: {base_type}",
316            base_type = structure_def.base_type
317        ));
318
319        if let Some(base_def) = &structure_def.base_definition {
320            lines.push(format!("- Base Definition: {base_def}"));
321        }
322
323        lines
324    }
325
326    /// Generate documentation for a trait based on StructureDefinition metadata
327    pub fn generate_trait_documentation(structure_def: &StructureDefinition) -> Option<String> {
328        let mut doc_lines = Vec::new();
329
330        // Add title if available, otherwise use name
331        let title = if let Some(title) = &structure_def.title {
332            format!("{title} Trait")
333        } else {
334            format!("{name} Trait", name = structure_def.name)
335        };
336        doc_lines.push(title);
337
338        // Add description if available
339        if let Some(description) = &structure_def.description {
340            doc_lines.push("".to_string());
341            doc_lines.push(
342                "This trait provides common functionality and access patterns for this FHIR resource type."
343                    .to_string(),
344            );
345            doc_lines.push("".to_string());
346            // Clean up the description by removing carriage returns and other problematic characters
347            let cleaned_description = Self::clean_description(description);
348            doc_lines.push(cleaned_description);
349        } else {
350            doc_lines.push("".to_string());
351            doc_lines.push(
352                "This trait provides common functionality and access patterns for this FHIR resource type."
353                    .to_string(),
354            );
355        }
356
357        // Add source information
358        let source_info = Self::generate_source_info_block(structure_def);
359        doc_lines.extend(source_info);
360
361        if doc_lines.is_empty() {
362            None
363        } else {
364            Some(doc_lines.join("\n"))
365        }
366    }
367}
368
369impl Default for DocumentationGenerator {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn test_struct_documentation_generation() {
381        let structure_def = StructureDefinition {
382            resource_type: "StructureDefinition".to_string(),
383            id: "Patient".to_string(),
384            url: "http://hl7.org/fhir/StructureDefinition/Patient".to_string(),
385            name: "Patient".to_string(),
386            title: Some("Patient Resource".to_string()),
387            status: "active".to_string(),
388            kind: "resource".to_string(),
389            is_abstract: false,
390            description: Some("Demographics and other administrative information about an individual receiving care.".to_string()),
391            purpose: None,
392            base_type: "DomainResource".to_string(),
393            base_definition: Some("http://hl7.org/fhir/StructureDefinition/DomainResource".to_string()),
394            version: Some("4.0.1".to_string()),
395            differential: None,
396            snapshot: None,
397        };
398
399        let doc = DocumentationGenerator::generate_struct_documentation(&structure_def);
400        assert!(doc.is_some());
401
402        let doc_text = doc.unwrap();
403        assert!(doc_text.contains("Patient Resource"));
404        assert!(doc_text.contains("Demographics and other administrative information"));
405        assert!(doc_text.contains("**Source:**"));
406        assert!(doc_text.contains("http://hl7.org/fhir/StructureDefinition/Patient"));
407        assert!(doc_text.contains("Version: 4.0.1"));
408        assert!(doc_text.contains("Kind: resource"));
409    }
410
411    #[test]
412    fn test_field_documentation_generation() {
413        use crate::fhir_types::ElementDefinition;
414
415        let element = ElementDefinition {
416            id: Some("Patient.active".to_string()),
417            path: "Patient.active".to_string(),
418            short: Some("Whether this patient record is in active use".to_string()),
419            definition: Some(
420                "Whether this patient record is in active use. Many systems...".to_string(),
421            ),
422            min: Some(0),
423            max: Some("1".to_string()),
424            element_type: None,
425            fixed: None,
426            pattern: None,
427            binding: None,
428            constraint: None,
429        };
430
431        let doc = DocumentationGenerator::generate_field_documentation(&element);
432        assert!(doc.is_some());
433        assert_eq!(doc.unwrap(), "Whether this patient record is in active use");
434    }
435
436    #[test]
437    fn test_primitive_element_documentation() {
438        let doc = DocumentationGenerator::generate_primitive_element_documentation("uri");
439        assert_eq!(
440            doc,
441            "Element structure for the 'uri' primitive type. Contains metadata and extensions."
442        );
443    }
444
445    #[test]
446    fn test_nested_struct_documentation() {
447        let doc = DocumentationGenerator::generate_nested_struct_documentation("Bundle", "entry");
448        assert_eq!(doc, "Bundle nested structure for the 'entry' field");
449    }
450
451    #[test]
452    fn test_clean_description() {
453        let dirty_description = "This is a test\r\nwith carriage returns\rand newlines\n.";
454        let clean = DocumentationGenerator::clean_description(dirty_description);
455        assert_eq!(clean, "This is a test with carriage returnsand newlines .");
456    }
457}