rh_codegen/generators/
documentation_generator.rs1use crate::fhir_types::{ElementDefinition, StructureDefinition};
7use crate::value_sets::ValueSetManager;
8
9pub struct DocumentationGenerator;
11
12impl DocumentationGenerator {
13 pub fn new() -> Self {
15 Self
16 }
17
18 pub fn generate_struct_documentation(structure_def: &StructureDefinition) -> Option<String> {
20 let mut doc_lines = Vec::new();
21
22 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 if let Some(description) = &structure_def.description {
31 doc_lines.push("".to_string());
32 let cleaned_description = description.replace('\r', "").replace('\n', " ");
34 doc_lines.push(cleaned_description);
35 }
36
37 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 pub fn generate_field_documentation(element: &ElementDefinition) -> Option<String> {
65 let mut doc_parts = Vec::new();
66
67 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 if let Some(binding) = &element.binding {
76 if binding.strength != "required" {
77 doc_parts.push(format!(
79 "Binding: {} ({})",
80 binding.strength,
81 binding.description.as_deref().unwrap_or("No description")
82 ));
83
84 }
86 }
87
88 if doc_parts.is_empty() {
89 None
90 } else {
91 Some(doc_parts.join("\n\n"))
92 }
93 }
94
95 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 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 if let Some(binding) = &element.binding {
111 if binding.strength != "required" {
112 doc_parts.push(format!(
114 "Binding: {} ({})",
115 binding.strength,
116 binding.description.as_deref().unwrap_or("No description")
117 ));
118
119 if let Some(value_set_url) = &binding.value_set {
121 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 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 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 pub fn generate_choice_field_documentation(
167 element: &ElementDefinition,
168 type_code: &str,
169 ) -> Option<String> {
170 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 Some(format!("{base_doc} ({type_code})"))
181 }
182
183 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 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 doc_parts.push(format!("{base_doc} ({type_code})"));
202
203 if type_code == "code" {
205 if let Some(binding) = &element.binding {
206 if binding.strength != "required" {
207 doc_parts.push(format!(
209 "Binding: {} ({})",
210 binding.strength,
211 binding.description.as_deref().unwrap_or("No description")
212 ));
213
214 if let Some(value_set_url) = &binding.value_set {
216 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 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 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 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 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 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 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 pub fn clean_description(description: &str) -> String {
298 description.replace('\r', "").replace('\n', " ")
299 }
300
301 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 pub fn generate_trait_documentation(structure_def: &StructureDefinition) -> Option<String> {
328 let mut doc_lines = Vec::new();
329
330 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 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 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 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}