Skip to main content

rh_codegen/generators/
file_io_manager.rs

1//! File I/O operations for FHIR code generation
2//!
3//! This module provides centralized file I/O operations for loading FHIR StructureDefinitions
4//! and generating code files in organized directory structures.
5
6use std::collections::HashMap;
7use std::fs;
8use std::path::Path;
9
10use crate::config::CodegenConfig;
11use crate::fhir_types::StructureDefinition;
12use crate::generators::{FileGenerator, TokenGenerator};
13use crate::rust_types::{RustStruct, RustTrait};
14use crate::CodegenResult;
15
16// Re-export for convenience
17pub use crate::generators::file_generator::FhirTypeCategory;
18
19/// Manager for file I/O operations in code generation
20pub struct FileIoManager<'a> {
21    config: &'a CodegenConfig,
22    token_generator: &'a TokenGenerator,
23}
24
25impl<'a> FileIoManager<'a> {
26    /// Create a new file I/O manager
27    pub fn new(config: &'a CodegenConfig, token_generator: &'a TokenGenerator) -> Self {
28        Self {
29            config,
30            token_generator,
31        }
32    }
33
34    /// Load and parse a FHIR StructureDefinition from a JSON file
35    pub fn load_structure_definition<P: AsRef<Path>>(
36        path: P,
37    ) -> CodegenResult<StructureDefinition> {
38        let content = fs::read_to_string(&path)?;
39
40        let structure_def: StructureDefinition = serde_json::from_str(&content)?;
41
42        Ok(structure_def)
43    }
44
45    /// Generate a Rust struct and write it to the appropriate directory based on FHIR type classification
46    pub fn generate_to_organized_directories<P: AsRef<Path>>(
47        &self,
48        structure_def: &StructureDefinition,
49        base_output_dir: P,
50        rust_struct: &RustStruct,
51        nested_structs: &[RustStruct],
52    ) -> CodegenResult<()> {
53        let base_dir = base_output_dir.as_ref();
54
55        // Determine the appropriate subdirectory based on FHIR type
56        let target_dir = match self.classify_fhir_structure_def(structure_def) {
57            FhirTypeCategory::Resource => base_dir.join("src").join("resources"),
58            FhirTypeCategory::Profile => base_dir.join("src").join("profiles"),
59            FhirTypeCategory::DataType => base_dir.join("src").join("datatypes"),
60            FhirTypeCategory::Extension => base_dir.join("src").join("extensions"),
61            FhirTypeCategory::Primitive => base_dir.join("src").join("primitives"),
62        };
63
64        // Ensure the target directory exists
65        std::fs::create_dir_all(&target_dir)?;
66
67        // Generate the file in the appropriate directory
68        let filename = crate::naming::Naming::filename(structure_def);
69        let output_path = target_dir.join(filename);
70
71        self.generate_to_file(structure_def, output_path, rust_struct, nested_structs)
72    }
73
74    /// Generate multiple traits and write them to the traits directory
75    pub fn generate_traits_to_organized_directory<P: AsRef<Path>>(
76        &self,
77        structure_def: &StructureDefinition,
78        base_output_dir: P,
79        rust_traits: &[RustTrait],
80    ) -> CodegenResult<()> {
81        let traits_dir = base_output_dir.as_ref().join("src").join("traits");
82
83        // Ensure the traits directory exists
84        std::fs::create_dir_all(&traits_dir)?;
85
86        // Generate the trait file
87        let struct_name = crate::naming::Naming::struct_name(structure_def);
88        let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
89        let filename = format!("{snake_case_name}.rs");
90        let output_path = traits_dir.join(filename);
91
92        // Convert Vec<RustTrait> to Vec<&RustTrait> for the method call
93        let rust_trait_refs: Vec<&RustTrait> = rust_traits.iter().collect();
94        let file_generator = FileGenerator::new(self.config, self.token_generator);
95        file_generator.generate_traits_to_file(structure_def, output_path, &rust_trait_refs)
96    }
97
98    /// Generate a trait and write it to the traits directory
99    pub fn generate_trait_to_organized_directory<P: AsRef<Path>>(
100        &self,
101        structure_def: &StructureDefinition,
102        base_output_dir: P,
103        rust_trait: &RustTrait,
104    ) -> CodegenResult<()> {
105        let traits_dir = base_output_dir.as_ref().join("src").join("traits");
106
107        // Ensure the traits directory exists
108        std::fs::create_dir_all(&traits_dir)?;
109
110        // Generate the trait file
111        let struct_name = crate::naming::Naming::struct_name(structure_def);
112        let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
113        let filename = format!("{snake_case_name}.rs");
114        let output_path = traits_dir.join(filename);
115
116        self.generate_trait_to_file(structure_def, output_path, rust_trait)
117    }
118
119    /// Generate a Rust struct and write it to a file
120    pub fn generate_to_file<P: AsRef<Path>>(
121        &self,
122        structure_def: &StructureDefinition,
123        output_path: P,
124        rust_struct: &RustStruct,
125        nested_structs: &[RustStruct],
126    ) -> CodegenResult<()> {
127        let file_generator = FileGenerator::new(self.config, self.token_generator);
128        file_generator.generate_to_file(structure_def, output_path, rust_struct, nested_structs)
129    }
130
131    /// Generate multiple Rust traits and write them to a file
132    pub fn generate_traits_to_file<P: AsRef<Path>>(
133        &self,
134        structure_def: &StructureDefinition,
135        output_path: P,
136        rust_traits: &[RustTrait],
137    ) -> CodegenResult<()> {
138        let file_generator = FileGenerator::new(self.config, self.token_generator);
139        // Convert Vec<RustTrait> to Vec<&RustTrait> for the method call
140        let rust_trait_refs: Vec<&RustTrait> = rust_traits.iter().collect();
141        file_generator.generate_traits_to_file(structure_def, output_path, &rust_trait_refs)
142    }
143
144    /// Generate a Rust trait and write it to a file
145    pub fn generate_trait_to_file<P: AsRef<Path>>(
146        &self,
147        structure_def: &StructureDefinition,
148        output_path: P,
149        rust_trait: &RustTrait,
150    ) -> CodegenResult<()> {
151        let file_generator = FileGenerator::new(self.config, self.token_generator);
152        file_generator.generate_trait_to_file(structure_def, output_path, rust_trait)
153    }
154
155    /// Generate a trait file directly from a RustTrait object
156    pub fn generate_trait_file_from_trait<P: AsRef<Path>>(
157        &self,
158        rust_trait: &RustTrait,
159        output_path: P,
160    ) -> CodegenResult<()> {
161        let file_generator = FileGenerator::new(self.config, self.token_generator);
162        file_generator.generate_trait_file_from_trait(rust_trait, output_path)
163    }
164
165    /// Check if a cached struct name is a direct nested struct of the parent struct
166    /// Uses TypeRegistry to determine actual parent-child relationships based on StructureDefinitions
167    fn is_direct_nested_struct(parent_name: &str, cached_name: &str) -> bool {
168        // First, use the TypeRegistry to check if this is actually a nested structure
169        // and if so, verify that it belongs to the specified parent
170        if let Some(classification) =
171            crate::generators::type_registry::TypeRegistry::get_classification(cached_name)
172        {
173            match classification {
174                crate::generators::type_registry::TypeClassification::NestedStructure {
175                    parent_resource,
176                } => {
177                    // This is a registered nested structure, check if the parent matches
178                    return parent_resource == parent_name;
179                }
180                _ => {
181                    // This is not a nested structure (it's a Resource, ComplexType, Profile, etc.)
182                    // so it should not be collected as a nested struct regardless of naming patterns
183                    return false;
184                }
185            }
186        }
187
188        // Fallback to name-based logic for unregistered types
189        // This handles cases where structures might not be registered yet during processing
190
191        // Must start with parent name
192        if !cached_name.starts_with(parent_name) {
193            return false;
194        }
195
196        // Must have something after the parent name
197        if cached_name.len() <= parent_name.len() {
198            return false;
199        }
200
201        // For unregistered types, we use a more conservative approach:
202        // Only consider it a nested struct if it looks like a typical nested structure pattern
203        // and doesn't match known patterns for separate resources
204        let remainder = &cached_name[parent_name.len()..];
205
206        // Must be a direct nested structure name, not a substring match
207        // e.g., "ElementBinding" from "Element" is valid (remainder="Binding")
208        // but "ElementdefinitionBinding" from "Element" is NOT valid
209        // (remainder="definitionBinding" which looks like it belongs to ElementDefinition)
210
211        // If remainder starts with lowercase letters, it's likely a separate structure
212        // e.g., "Element" + "definitionBinding" suggests it belongs to "ElementDefinition"
213        if let Some(first_char) = remainder.chars().next() {
214            if first_char.is_lowercase() {
215                return false;
216            }
217        }
218
219        // If the remainder is a single common resource suffix, it's likely a separate resource
220        let separate_resource_suffixes = [
221            "Definition",
222            "Specification",
223            "Polymer",
224            "Protein",
225            "ReferenceInformation",
226            "SourceMaterial",
227            "NucleicAcid",
228            "Authorization",
229            "Contraindication",
230            "Indication",
231            "Ingredient",
232            "Interaction",
233            "Manufactured",
234            "Packaged",
235            "Pharmaceutical",
236            "UndesirableEffect",
237            "Knowledge",
238            "Administration",
239            "Dispense",
240            "Request",
241            "Statement",
242        ];
243
244        if separate_resource_suffixes.contains(&remainder) {
245            return false;
246        }
247
248        // If it starts with these suffixes, it's likely part of a separate resource family
249        if separate_resource_suffixes
250            .iter()
251            .any(|&suffix| remainder.starts_with(suffix))
252        {
253            return false;
254        }
255
256        true
257    }
258
259    /// Collect nested structs from the type cache that are related to a main struct
260    pub fn collect_nested_structs(
261        struct_name: &str,
262        type_cache: &HashMap<String, RustStruct>,
263    ) -> Vec<RustStruct> {
264        let mut nested_structs = vec![];
265
266        // Find all nested structs that start with the main struct name
267        // but ensure they are direct children, not just sharing a prefix
268        for (cached_name, cached_struct) in type_cache {
269            if cached_name != struct_name && Self::is_direct_nested_struct(struct_name, cached_name)
270            {
271                nested_structs.push(cached_struct.clone());
272            }
273        }
274
275        nested_structs.sort_by(|left, right| left.name.cmp(&right.name));
276        nested_structs
277    }
278
279    /// Classify a FHIR StructureDefinition into the appropriate category
280    pub fn classify_fhir_structure_def(
281        &self,
282        structure_def: &StructureDefinition,
283    ) -> FhirTypeCategory {
284        let file_generator = FileGenerator::new(self.config, self.token_generator);
285        file_generator.classify_fhir_structure_def(structure_def)
286    }
287
288    /// Ensure the given directory exists, creating it if necessary
289    pub fn ensure_directory(dir_path: &Path) -> CodegenResult<()> {
290        std::fs::create_dir_all(dir_path)?;
291        Ok(())
292    }
293
294    /// Generate the appropriate file path for a structure definition
295    pub fn get_output_path_for_structure<P: AsRef<Path>>(
296        base_dir: P,
297        structure_def: &StructureDefinition,
298        file_generator: &FileGenerator,
299    ) -> std::path::PathBuf {
300        let base_dir = base_dir.as_ref();
301
302        // Determine the appropriate subdirectory based on FHIR type
303        let target_dir = match file_generator.classify_fhir_structure_def(structure_def) {
304            FhirTypeCategory::Resource => base_dir.join("src").join("resources"),
305            FhirTypeCategory::Profile => base_dir.join("src").join("profiles"),
306            FhirTypeCategory::DataType => base_dir.join("src").join("datatypes"),
307            FhirTypeCategory::Extension => base_dir.join("src").join("extensions"),
308            FhirTypeCategory::Primitive => base_dir.join("src").join("primitives"),
309        };
310
311        let filename = crate::naming::Naming::filename(structure_def);
312        target_dir.join(filename)
313    }
314
315    /// Generate the traits directory path
316    pub fn get_traits_directory_path<P: AsRef<Path>>(base_dir: P) -> std::path::PathBuf {
317        base_dir.as_ref().join("src").join("traits")
318    }
319
320    /// Generate the trait file path for a structure definition
321    pub fn get_trait_file_path<P: AsRef<Path>>(
322        base_dir: P,
323        structure_def: &StructureDefinition,
324    ) -> std::path::PathBuf {
325        let traits_dir = Self::get_traits_directory_path(base_dir);
326        let struct_name = crate::naming::Naming::struct_name(structure_def);
327        let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
328        let filename = format!("{snake_case_name}.rs");
329        traits_dir.join(filename)
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use crate::config::CodegenConfig;
337    use crate::generators::TokenGenerator;
338    use tempfile::TempDir;
339
340    #[test]
341    fn test_load_structure_definition() {
342        use std::fs;
343
344        let temp_dir = TempDir::new().unwrap();
345        let file_path = temp_dir.path().join("test_structure.json");
346
347        // Create a minimal valid StructureDefinition JSON manually
348        let json_content = r#"{
349            "resourceType": "StructureDefinition",
350            "id": "Patient",
351            "url": "http://hl7.org/fhir/StructureDefinition/Patient",
352            "name": "Patient",
353            "title": "Patient",
354            "status": "active",
355            "kind": "resource",
356            "abstract": false,
357            "type": "Patient",
358            "description": "A patient resource",
359            "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
360        }"#;
361
362        fs::write(&file_path, json_content).unwrap();
363
364        // Test loading
365        let result = FileIoManager::load_structure_definition(&file_path);
366        assert!(
367            result.is_ok(),
368            "Should load StructureDefinition successfully"
369        );
370
371        let loaded_structure = result.unwrap();
372        assert_eq!(loaded_structure.name, "Patient");
373        assert_eq!(loaded_structure.kind, "resource");
374    }
375
376    #[test]
377    fn test_load_structure_definition_invalid_json() {
378        let temp_dir = TempDir::new().unwrap();
379        let file_path = temp_dir.path().join("invalid.json");
380
381        // Write invalid JSON
382        fs::write(&file_path, "{ invalid json }").unwrap();
383
384        let result = FileIoManager::load_structure_definition(&file_path);
385        assert!(result.is_err(), "Should fail to load invalid JSON");
386    }
387
388    #[test]
389    fn test_load_structure_definition_missing_file() {
390        let temp_dir = TempDir::new().unwrap();
391        let file_path = temp_dir.path().join("missing.json");
392
393        let result = FileIoManager::load_structure_definition(&file_path);
394        assert!(result.is_err(), "Should fail to load missing file");
395    }
396
397    #[test]
398    fn test_collect_nested_structs() {
399        let mut type_cache = HashMap::new();
400
401        // Add main struct
402        let patient_struct = RustStruct::new("Patient".to_string());
403        type_cache.insert("Patient".to_string(), patient_struct);
404
405        // Add nested structs
406        let patient_contact_struct = RustStruct::new("PatientContact".to_string());
407        type_cache.insert("PatientContact".to_string(), patient_contact_struct);
408
409        let patient_link_struct = RustStruct::new("PatientLink".to_string());
410        type_cache.insert("PatientLink".to_string(), patient_link_struct);
411
412        // Add unrelated struct
413        let observation_struct = RustStruct::new("Observation".to_string());
414        type_cache.insert("Observation".to_string(), observation_struct);
415
416        let nested_structs = FileIoManager::collect_nested_structs("Patient", &type_cache);
417
418        assert_eq!(
419            nested_structs.len(),
420            2,
421            "Should collect exactly 2 nested structs"
422        );
423
424        let nested_names: Vec<String> = nested_structs.iter().map(|s| s.name.clone()).collect();
425        assert!(nested_names.contains(&"PatientContact".to_string()));
426        assert!(nested_names.contains(&"PatientLink".to_string()));
427        assert!(!nested_names.contains(&"Patient".to_string())); // Shouldn't include main struct
428        assert!(!nested_names.contains(&"Observation".to_string())); // Shouldn't include unrelated struct
429    }
430
431    #[test]
432    fn test_ensure_directory_exists() {
433        let temp_dir = TempDir::new().unwrap();
434        let new_dir = temp_dir.path().join("nested").join("directory");
435
436        // Directory shouldn't exist initially
437        assert!(!new_dir.exists());
438
439        let result = FileIoManager::ensure_directory(&new_dir);
440        assert!(result.is_ok(), "Should create directory successfully");
441
442        // Directory should now exist
443        assert!(new_dir.exists());
444        assert!(new_dir.is_dir());
445    }
446
447    #[test]
448    fn test_get_output_path_for_structure() {
449        let config = CodegenConfig::default();
450        let token_generator = TokenGenerator::new();
451        let file_generator = FileGenerator::new(&config, &token_generator);
452
453        let temp_dir = TempDir::new().unwrap();
454
455        // Create test JSON content manually
456        let json_content = r#"{
457            "resourceType": "StructureDefinition",
458            "id": "Patient",
459            "url": "http://hl7.org/fhir/StructureDefinition/Patient",
460            "name": "Patient",
461            "title": "Patient",
462            "status": "active",
463            "kind": "resource",
464            "abstract": false,
465            "type": "Patient",
466            "description": "A patient resource",
467            "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
468        }"#;
469
470        // Parse the structure definition
471        let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
472
473        let output_path = FileIoManager::get_output_path_for_structure(
474            temp_dir.path(),
475            &patient_structure,
476            &file_generator,
477        );
478
479        let expected_path = temp_dir
480            .path()
481            .join("src")
482            .join("resources")
483            .join("patient.rs");
484        assert_eq!(output_path, expected_path);
485    }
486
487    #[test]
488    fn test_get_trait_file_path() {
489        let temp_dir = TempDir::new().unwrap();
490
491        // Create test JSON content manually
492        let json_content = r#"{
493            "resourceType": "StructureDefinition",
494            "id": "Patient",
495            "url": "http://hl7.org/fhir/StructureDefinition/Patient",
496            "name": "Patient",
497            "title": "Patient",
498            "status": "active",
499            "kind": "resource",
500            "abstract": false,
501            "type": "Patient",
502            "description": "A patient resource",
503            "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
504        }"#;
505
506        // Parse the structure definition
507        let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
508
509        let trait_path = FileIoManager::get_trait_file_path(temp_dir.path(), &patient_structure);
510        let expected_path = temp_dir
511            .path()
512            .join("src")
513            .join("traits")
514            .join("patient.rs");
515        assert_eq!(trait_path, expected_path);
516    }
517
518    #[test]
519    fn test_file_io_manager_creation() {
520        let config = CodegenConfig::default();
521        let token_generator = TokenGenerator::new();
522        let file_io_manager = FileIoManager::new(&config, &token_generator);
523
524        // Test that the manager can be created successfully
525        // This is mainly to ensure the lifetime parameters work correctly
526        // If this compiles and runs, the test passes
527        assert_eq!(
528            std::mem::size_of_val(&file_io_manager),
529            std::mem::size_of::<FileIoManager>()
530        );
531    }
532
533    #[test]
534    fn test_nested_struct_collection_fix() {
535        use crate::rust_types::RustStruct;
536        use std::collections::HashMap;
537
538        let mut type_cache = HashMap::new();
539
540        // Create main structs
541        let element_struct = RustStruct::new("Element".to_string());
542        let element_definition_struct = RustStruct::new("ElementDefinition".to_string());
543
544        // Create nested structs for Element
545        let element_extension_struct = RustStruct::new("ElementExtension".to_string());
546        let element_binding_struct = RustStruct::new("ElementBinding".to_string());
547
548        // Create nested structs for ElementDefinition (these should NOT be collected for Element)
549        let element_definition_binding_struct =
550            RustStruct::new("ElementDefinitionBinding".to_string());
551        let element_definition_constraint_struct =
552            RustStruct::new("ElementDefinitionConstraint".to_string());
553        let element_definition_type_struct = RustStruct::new("ElementDefinitionType".to_string());
554
555        // Add all structs to cache
556        type_cache.insert("Element".to_string(), element_struct);
557        type_cache.insert("ElementDefinition".to_string(), element_definition_struct);
558        type_cache.insert("ElementExtension".to_string(), element_extension_struct);
559        type_cache.insert("ElementBinding".to_string(), element_binding_struct);
560        type_cache.insert(
561            "ElementDefinitionBinding".to_string(),
562            element_definition_binding_struct,
563        );
564        type_cache.insert(
565            "ElementDefinitionConstraint".to_string(),
566            element_definition_constraint_struct,
567        );
568        type_cache.insert(
569            "ElementDefinitionType".to_string(),
570            element_definition_type_struct,
571        );
572
573        // Add lowercase version of ElementDefinition structs (as they appear in real FHIR data)
574        let elementdefinition_binding_struct =
575            RustStruct::new("ElementdefinitionBinding".to_string());
576        type_cache.insert(
577            "ElementdefinitionBinding".to_string(),
578            elementdefinition_binding_struct,
579        );
580        let elementdefinition_constraint_struct =
581            RustStruct::new("ElementdefinitionConstraint".to_string());
582        type_cache.insert(
583            "ElementdefinitionConstraint".to_string(),
584            elementdefinition_constraint_struct,
585        );
586        let elementdefinition_type_struct = RustStruct::new("ElementdefinitionType".to_string());
587        type_cache.insert(
588            "ElementdefinitionType".to_string(),
589            elementdefinition_type_struct,
590        );
591
592        // Test collecting nested structs for Element
593        let element_nested = FileIoManager::collect_nested_structs("Element", &type_cache);
594
595        // Element should only collect ElementExtension and ElementBinding,
596        // NOT ElementDefinition* structs
597        assert_eq!(
598            element_nested.len(),
599            2,
600            "Element should have 2 nested structs"
601        );
602
603        let element_nested_names: Vec<String> =
604            element_nested.iter().map(|s| s.name.clone()).collect();
605        assert!(element_nested_names.contains(&"ElementExtension".to_string()));
606        assert!(element_nested_names.contains(&"ElementBinding".to_string()));
607        assert!(!element_nested_names.contains(&"ElementDefinitionBinding".to_string()));
608        assert!(!element_nested_names.contains(&"ElementDefinitionConstraint".to_string()));
609        assert!(!element_nested_names.contains(&"ElementDefinitionType".to_string()));
610        assert!(!element_nested_names.contains(&"ElementDefinition".to_string()));
611        // Test that lowercase versions are also excluded
612        assert!(!element_nested_names.contains(&"ElementdefinitionBinding".to_string()));
613        assert!(!element_nested_names.contains(&"ElementdefinitionConstraint".to_string()));
614        assert!(!element_nested_names.contains(&"ElementdefinitionType".to_string()));
615
616        // Test collecting nested structs for ElementDefinition
617        let element_definition_nested =
618            FileIoManager::collect_nested_structs("ElementDefinition", &type_cache);
619
620        // ElementDefinition should collect all ElementDefinition* structs (uppercase),
621        // but NOT Elementdefinition* structs (lowercase) because they are separate entities
622        assert_eq!(
623            element_definition_nested.len(),
624            3,
625            "ElementDefinition should have 3 nested structs (only uppercase)"
626        );
627
628        let element_definition_nested_names: Vec<String> = element_definition_nested
629            .iter()
630            .map(|s| s.name.clone())
631            .collect();
632        assert!(element_definition_nested_names.contains(&"ElementDefinitionBinding".to_string()));
633        assert!(
634            element_definition_nested_names.contains(&"ElementDefinitionConstraint".to_string())
635        );
636        assert!(element_definition_nested_names.contains(&"ElementDefinitionType".to_string()));
637        // Test that lowercase versions are NOT collected by ElementDefinition because they are separate entities
638        assert!(!element_definition_nested_names.contains(&"ElementdefinitionBinding".to_string()));
639        assert!(
640            !element_definition_nested_names.contains(&"ElementdefinitionConstraint".to_string())
641        );
642        assert!(!element_definition_nested_names.contains(&"ElementdefinitionType".to_string()));
643        assert!(!element_definition_nested_names.contains(&"ElementExtension".to_string()));
644        assert!(!element_definition_nested_names.contains(&"ElementBinding".to_string()));
645
646        // Test some edge cases to ensure the logic is robust
647
648        // Test that Bundle correctly collects BundleEntry but not BundleDefinition (if it existed)
649        type_cache.insert("Bundle".to_string(), RustStruct::new("Bundle".to_string()));
650        type_cache.insert(
651            "BundleEntry".to_string(),
652            RustStruct::new("BundleEntry".to_string()),
653        );
654        type_cache.insert(
655            "BundleLink".to_string(),
656            RustStruct::new("BundleLink".to_string()),
657        );
658
659        let bundle_nested = FileIoManager::collect_nested_structs("Bundle", &type_cache);
660        assert_eq!(
661            bundle_nested.len(),
662            2,
663            "Bundle should have 2 nested structs"
664        );
665
666        let bundle_nested_names: Vec<String> =
667            bundle_nested.iter().map(|s| s.name.clone()).collect();
668        assert!(bundle_nested_names.contains(&"BundleEntry".to_string()));
669        assert!(bundle_nested_names.contains(&"BundleLink".to_string()));
670    }
671}