Skip to main content

rh_codegen/generators/
nested_struct_generator.rs

1//! Nested struct generation functionality for FHIR BackboneElements
2//!
3//! This module provides functionality for generating nested structs from FHIR BackboneElements,
4//! which are complex structures that appear within other FHIR structures.
5
6use std::collections::HashMap;
7
8use crate::config::CodegenConfig;
9use crate::fhir_types::{ElementDefinition, StructureDefinition};
10use crate::generators::{DocumentationGenerator, FieldGenerator, TypeRegistry};
11use crate::naming::Naming;
12use crate::rust_types::RustStruct;
13use crate::CodegenResult;
14
15/// Generator for nested struct types from FHIR BackboneElements
16pub struct NestedStructGenerator<'a> {
17    config: &'a CodegenConfig,
18    type_cache: &'a mut HashMap<String, RustStruct>,
19    value_set_manager: &'a mut crate::value_sets::ValueSetManager,
20}
21
22impl<'a> NestedStructGenerator<'a> {
23    /// Create a new nested struct generator
24    pub fn new(
25        config: &'a CodegenConfig,
26        type_cache: &'a mut HashMap<String, RustStruct>,
27        value_set_manager: &'a mut crate::value_sets::ValueSetManager,
28    ) -> Self {
29        Self {
30            config,
31            type_cache,
32            value_set_manager,
33        }
34    }
35
36    /// Generate a nested struct for BackboneElements
37    pub fn generate_nested_struct(
38        &mut self,
39        parent_struct_name: &str,
40        nested_field_name: &str,
41        nested_elements: &[ElementDefinition],
42        parent_structure_def: &StructureDefinition,
43    ) -> CodegenResult<Option<RustStruct>> {
44        // Generate the nested struct name (e.g., BundleEntry, BundleLink)
45        let nested_struct_name = format!(
46            "{}{}",
47            parent_struct_name,
48            Naming::to_pascal_case(nested_field_name)
49        );
50
51        // Check if we've already generated this nested struct
52        if self.type_cache.contains_key(&nested_struct_name) {
53            return Ok(None);
54        }
55
56        // Create the nested struct
57        let mut nested_struct = RustStruct::new(nested_struct_name.clone());
58
59        // Add documentation
60        nested_struct.doc_comment = Some(
61            DocumentationGenerator::generate_nested_struct_documentation(
62                parent_struct_name,
63                nested_field_name,
64            ),
65        );
66
67        // Use config to determine derives
68        let mut derives = vec!["Debug".to_string(), "Clone".to_string()];
69        if self.config.with_serde {
70            derives.extend(vec!["Serialize".to_string(), "Deserialize".to_string()]);
71        }
72        nested_struct.derives = derives;
73
74        // Set base as BackboneElement since these are typically BackboneElements
75        nested_struct.base_definition = Some("BackboneElement".to_string());
76
77        // Process the nested elements
78        let base_path = format!("{}.{}", parent_structure_def.name, nested_field_name);
79        let mut sub_nested_structs = HashMap::new();
80        let mut direct_fields = Vec::new();
81
82        for element in nested_elements {
83            if !element.path.starts_with(&base_path) {
84                continue;
85            }
86
87            let field_path = element.path.strip_prefix(&format!("{base_path}.")).unwrap();
88
89            if field_path.contains('.') {
90                // This is a sub-nested field - collect it for recursive nested struct generation
91                let sub_nested_field_name = field_path.split('.').next().unwrap();
92                sub_nested_structs
93                    .entry(sub_nested_field_name.to_string())
94                    .or_insert_with(Vec::new)
95                    .push(element.clone());
96            } else {
97                // Check if this direct field is itself a BackboneElement that needs a nested struct
98                let is_backbone_element = element
99                    .element_type
100                    .as_ref()
101                    .and_then(|types| types.first())
102                    .and_then(|t| t.code.as_ref())
103                    .map(|code| code == "BackboneElement")
104                    .unwrap_or(false);
105
106                if is_backbone_element {
107                    // Treat this as a sub-nested struct even though it appears as a direct field
108                    sub_nested_structs
109                        .entry(field_path.to_string())
110                        .or_insert_with(Vec::new)
111                        .push(element.clone());
112                } else {
113                    // This is a direct field of this nested struct
114                    direct_fields.push(element.clone());
115                }
116            }
117        }
118
119        // First, generate any sub-nested structs
120        for (sub_nested_field_name, sub_nested_elements) in &sub_nested_structs {
121            self.generate_sub_nested_struct(
122                &nested_struct_name,
123                sub_nested_field_name,
124                sub_nested_elements,
125                &base_path,
126            )?;
127        }
128
129        // Then, process direct fields (now sub-nested structs are available)
130        for element in direct_fields {
131            if let Some(field) = self.create_field_from_element(&element)? {
132                nested_struct.add_field(field);
133            }
134        }
135
136        // Store the nested struct in cache before returning
137        self.type_cache
138            .insert(nested_struct_name.clone(), nested_struct.clone());
139
140        // Register the nested struct in TypeRegistry with proper classification
141        // Get the parent resource name from the parent struct name
142        let parent_resource = parent_struct_name.to_string();
143        TypeRegistry::register_type_classification_only(
144            &nested_struct_name,
145            crate::generators::type_registry::TypeClassification::NestedStructure {
146                parent_resource,
147            },
148        );
149
150        Ok(Some(nested_struct))
151    }
152
153    /// Generate a sub-nested struct (nested within a nested struct)
154    fn generate_sub_nested_struct(
155        &mut self,
156        nested_struct_name: &str,
157        sub_nested_field_name: &str,
158        sub_nested_elements: &[ElementDefinition],
159        base_path: &str,
160    ) -> CodegenResult<()> {
161        // For recursive calls, we need to create a modified context
162        // The base path for sub-nested structs should be the current nested struct's path
163        let sub_nested_struct_name = format!(
164            "{}{}",
165            nested_struct_name,
166            Naming::to_pascal_case(sub_nested_field_name)
167        );
168
169        if !self.type_cache.contains_key(&sub_nested_struct_name) {
170            let mut sub_nested_struct = RustStruct::new(sub_nested_struct_name.clone());
171
172            sub_nested_struct.doc_comment = Some(
173                DocumentationGenerator::generate_sub_nested_struct_documentation(
174                    nested_struct_name,
175                    sub_nested_field_name,
176                ),
177            );
178
179            // Use config to determine derives
180            let mut derives = vec!["Debug".to_string(), "Clone".to_string()];
181            if self.config.with_serde {
182                derives.extend(vec!["Serialize".to_string(), "Deserialize".to_string()]);
183            }
184            sub_nested_struct.derives = derives;
185            sub_nested_struct.base_definition = Some("BackboneElement".to_string());
186
187            // Process the sub-nested elements with full recursive support
188            let sub_base_path = format!("{base_path}.{sub_nested_field_name}");
189
190            // Separate sub-nested elements into direct fields and further sub-nested structures
191            let mut direct_fields = Vec::new();
192            let mut sub_sub_nested_structs: HashMap<String, Vec<ElementDefinition>> =
193                HashMap::new();
194
195            for element in sub_nested_elements {
196                if !element.path.starts_with(&sub_base_path) {
197                    continue;
198                }
199
200                let field_path = element
201                    .path
202                    .strip_prefix(&format!("{sub_base_path}."))
203                    .unwrap();
204
205                if field_path.contains('.') {
206                    // This is a further sub-nested field - collect it for recursive generation
207                    let sub_sub_nested_field_name = field_path.split('.').next().unwrap();
208                    sub_sub_nested_structs
209                        .entry(sub_sub_nested_field_name.to_string())
210                        .or_default()
211                        .push(element.clone());
212                } else {
213                    // Check if this direct field is itself a BackboneElement that needs a nested struct
214                    let is_backbone_element = element
215                        .element_type
216                        .as_ref()
217                        .and_then(|types| types.first())
218                        .and_then(|t| t.code.as_ref())
219                        .map(|code| code == "BackboneElement")
220                        .unwrap_or(false);
221
222                    if is_backbone_element {
223                        // Treat this as a further sub-nested struct even though it appears as a direct field
224                        sub_sub_nested_structs
225                            .entry(field_path.to_string())
226                            .or_default()
227                            .push(element.clone());
228                    } else {
229                        // This is a direct field of this sub-nested struct
230                        direct_fields.push(element.clone());
231                    }
232                }
233            }
234
235            // First, generate any further sub-nested structs recursively
236            for (sub_sub_nested_field_name, sub_sub_nested_elements) in &sub_sub_nested_structs {
237                self.generate_sub_nested_struct(
238                    &sub_nested_struct_name,
239                    sub_sub_nested_field_name,
240                    sub_sub_nested_elements,
241                    &sub_base_path,
242                )?;
243            } // Then, process direct fields (now further sub-nested structs are available)
244            for element in direct_fields {
245                if let Some(field) = self.create_field_from_element(&element)? {
246                    sub_nested_struct.add_field(field);
247                }
248            }
249
250            // Store the sub-nested struct in cache
251            self.type_cache
252                .insert(sub_nested_struct_name.clone(), sub_nested_struct);
253
254            // Register the sub-nested struct in TypeRegistry with proper classification
255            // Extract the parent resource from the nested_struct_name
256            // Use a simple approach: extract from the beginning of nested_struct_name
257            let parent_resource = Self::extract_parent_resource_name(nested_struct_name);
258
259            TypeRegistry::register_type_classification_only(
260                &sub_nested_struct_name,
261                crate::generators::type_registry::TypeClassification::NestedStructure {
262                    parent_resource,
263                },
264            );
265        }
266
267        Ok(())
268    }
269
270    /// Extract parent resource name from a nested structure name
271    /// For example: "ActivityDefinitionParticipant" -> "ActivityDefinition"
272    fn extract_parent_resource_name(nested_struct_name: &str) -> String {
273        // Use the TypeRegistry's method for consistency
274        crate::generators::type_registry::TypeRegistry::extract_parent_from_name(nested_struct_name)
275            .unwrap_or_else(|| nested_struct_name.to_string())
276    }
277
278    /// Create a RustField from an ElementDefinition
279    fn create_field_from_element(
280        &mut self,
281        element: &ElementDefinition,
282    ) -> CodegenResult<Option<crate::rust_types::RustField>> {
283        let mut field_generator =
284            FieldGenerator::new(self.config, self.type_cache, self.value_set_manager);
285        field_generator.create_field_from_element(element)
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use crate::config::CodegenConfig;
293    use crate::fhir_types::{
294        ElementDefinition, ElementType, StructureDefinition, StructureDefinitionDifferential,
295    };
296    use crate::value_sets::ValueSetManager;
297
298    #[test]
299    fn test_nested_struct_generation() {
300        let config = CodegenConfig::default();
301        let mut type_cache = HashMap::new();
302        let mut value_set_manager = ValueSetManager::new();
303        let mut generator =
304            NestedStructGenerator::new(&config, &mut type_cache, &mut value_set_manager);
305
306        // Create a simplified Bundle StructureDefinition with nested entry
307        let bundle_structure = StructureDefinition {
308            resource_type: "StructureDefinition".to_string(),
309            id: "Bundle".to_string(),
310            url: "http://hl7.org/fhir/StructureDefinition/Bundle".to_string(),
311            name: "Bundle".to_string(),
312            title: Some("Bundle".to_string()),
313            status: "active".to_string(),
314            kind: "resource".to_string(),
315            is_abstract: false,
316            description: Some("A container for a collection of resources".to_string()),
317            purpose: None,
318            base_type: "Bundle".to_string(),
319            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Resource".to_string()),
320            version: None,
321            differential: Some(StructureDefinitionDifferential {
322                element: vec![
323                    ElementDefinition {
324                        id: Some("Bundle.entry".to_string()),
325                        path: "Bundle.entry".to_string(),
326                        short: Some("Entry in the bundle".to_string()),
327                        definition: None,
328                        min: Some(0),
329                        max: Some("*".to_string()),
330                        element_type: Some(vec![ElementType {
331                            code: Some("BackboneElement".to_string()),
332                            target_profile: None,
333                        }]),
334                        fixed: None,
335                        pattern: None,
336                        binding: None,
337                        constraint: None,
338                    },
339                    ElementDefinition {
340                        id: Some("Bundle.entry.resource".to_string()),
341                        path: "Bundle.entry.resource".to_string(),
342                        short: Some("A resource in the bundle".to_string()),
343                        definition: None,
344                        min: Some(0),
345                        max: Some("1".to_string()),
346                        element_type: Some(vec![ElementType {
347                            code: Some("Resource".to_string()),
348                            target_profile: None,
349                        }]),
350                        fixed: None,
351                        pattern: None,
352                        binding: None,
353                        constraint: None,
354                    },
355                ],
356            }),
357            snapshot: None,
358        };
359
360        let nested_elements = bundle_structure
361            .differential
362            .as_ref()
363            .unwrap()
364            .element
365            .clone();
366
367        // Filter to only include the sub-elements of Bundle.entry (not Bundle.entry itself)
368        let filtered_elements: Vec<_> = nested_elements
369            .into_iter()
370            .filter(|e| e.path.starts_with("Bundle.entry."))
371            .collect();
372
373        // Generate the nested struct
374        let result = generator.generate_nested_struct(
375            "Bundle",
376            "entry",
377            &filtered_elements,
378            &bundle_structure,
379        );
380        assert!(result.is_ok(), "Should generate nested struct successfully");
381
382        let bundle_entry_struct = result.unwrap();
383        assert!(
384            bundle_entry_struct.is_some(),
385            "Should return a nested struct"
386        );
387
388        let bundle_entry_struct = bundle_entry_struct.unwrap();
389        assert_eq!(bundle_entry_struct.name, "BundleEntry");
390        assert_eq!(
391            bundle_entry_struct.base_definition,
392            Some("BackboneElement".to_string())
393        );
394
395        // Check that the struct was added to the type cache
396        assert!(
397            type_cache.contains_key("BundleEntry"),
398            "BundleEntry should be cached"
399        );
400
401        // Check that the nested struct has the expected fields
402        let resource_field = bundle_entry_struct
403            .fields
404            .iter()
405            .find(|f| f.name == "resource");
406        assert!(
407            resource_field.is_some(),
408            "BundleEntry should have a resource field"
409        );
410    }
411
412    #[test]
413    fn test_nested_struct_caching() {
414        let config = CodegenConfig::default();
415        let mut type_cache = HashMap::new();
416        let mut value_set_manager = ValueSetManager::new();
417
418        // Create a dummy nested struct and add it to cache
419        let existing_struct = RustStruct::new("BundleEntry".to_string());
420        type_cache.insert("BundleEntry".to_string(), existing_struct);
421
422        let mut generator =
423            NestedStructGenerator::new(&config, &mut type_cache, &mut value_set_manager);
424
425        let bundle_structure = StructureDefinition {
426            resource_type: "StructureDefinition".to_string(),
427            id: "Bundle".to_string(),
428            url: "http://hl7.org/fhir/StructureDefinition/Bundle".to_string(),
429            name: "Bundle".to_string(),
430            title: Some("Bundle".to_string()),
431            status: "active".to_string(),
432            kind: "resource".to_string(),
433            is_abstract: false,
434            description: Some("A container for a collection of resources".to_string()),
435            purpose: None,
436            base_type: "Bundle".to_string(),
437            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Resource".to_string()),
438            version: None,
439            differential: None,
440            snapshot: None,
441        };
442
443        let nested_elements = vec![];
444
445        // Generate the nested struct - should return None due to caching
446        let result = generator.generate_nested_struct(
447            "Bundle",
448            "entry",
449            &nested_elements,
450            &bundle_structure,
451        );
452        assert!(result.is_ok(), "Should handle cached struct successfully");
453
454        let bundle_entry_struct = result.unwrap();
455        assert!(
456            bundle_entry_struct.is_none(),
457            "Should return None for cached struct"
458        );
459    }
460
461    #[test]
462    fn test_nested_struct_documentation() {
463        let config = CodegenConfig::default();
464        let mut type_cache = HashMap::new();
465        let mut value_set_manager = ValueSetManager::new();
466        let mut generator =
467            NestedStructGenerator::new(&config, &mut type_cache, &mut value_set_manager);
468
469        let bundle_structure = StructureDefinition {
470            resource_type: "StructureDefinition".to_string(),
471            id: "Bundle".to_string(),
472            url: "http://hl7.org/fhir/StructureDefinition/Bundle".to_string(),
473            name: "Bundle".to_string(),
474            title: Some("Bundle".to_string()),
475            status: "active".to_string(),
476            kind: "resource".to_string(),
477            is_abstract: false,
478            description: Some("A container for a collection of resources".to_string()),
479            purpose: None,
480            base_type: "Bundle".to_string(),
481            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Resource".to_string()),
482            version: None,
483            differential: None,
484            snapshot: None,
485        };
486
487        let nested_elements = vec![];
488
489        // Generate the nested struct
490        let result = generator.generate_nested_struct(
491            "Bundle",
492            "entry",
493            &nested_elements,
494            &bundle_structure,
495        );
496        assert!(result.is_ok(), "Should generate nested struct successfully");
497
498        let bundle_entry_struct = result.unwrap().unwrap();
499
500        // Check documentation was generated
501        assert!(
502            bundle_entry_struct.doc_comment.is_some(),
503            "Should have documentation"
504        );
505        let doc = bundle_entry_struct.doc_comment.unwrap();
506        assert!(
507            doc.contains("Bundle"),
508            "Documentation should mention parent struct"
509        );
510        assert!(
511            doc.contains("entry"),
512            "Documentation should mention field name"
513        );
514    }
515
516    #[test]
517    fn test_nested_struct_derives() {
518        let config = CodegenConfig {
519            with_serde: true,
520            ..Default::default()
521        };
522        let mut type_cache = HashMap::new();
523        let mut value_set_manager = ValueSetManager::new();
524        let mut generator =
525            NestedStructGenerator::new(&config, &mut type_cache, &mut value_set_manager);
526
527        let bundle_structure = StructureDefinition {
528            resource_type: "StructureDefinition".to_string(),
529            id: "Bundle".to_string(),
530            url: "http://hl7.org/fhir/StructureDefinition/Bundle".to_string(),
531            name: "Bundle".to_string(),
532            title: Some("Bundle".to_string()),
533            status: "active".to_string(),
534            kind: "resource".to_string(),
535            is_abstract: false,
536            description: Some("A container for a collection of resources".to_string()),
537            purpose: None,
538            base_type: "Bundle".to_string(),
539            base_definition: Some("http://hl7.org/fhir/StructureDefinition/Resource".to_string()),
540            version: None,
541            differential: None,
542            snapshot: None,
543        };
544
545        let nested_elements = vec![];
546
547        // Generate the nested struct
548        let result = generator.generate_nested_struct(
549            "Bundle",
550            "entry",
551            &nested_elements,
552            &bundle_structure,
553        );
554        assert!(result.is_ok(), "Should generate nested struct successfully");
555
556        let bundle_entry_struct = result.unwrap().unwrap();
557
558        // Check derives include serde when enabled
559        assert!(bundle_entry_struct.derives.contains(&"Debug".to_string()));
560        assert!(bundle_entry_struct.derives.contains(&"Clone".to_string()));
561        assert!(bundle_entry_struct
562            .derives
563            .contains(&"Serialize".to_string()));
564        assert!(bundle_entry_struct
565            .derives
566            .contains(&"Deserialize".to_string()));
567    }
568}