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
276    }
277
278    /// Classify a FHIR StructureDefinition into the appropriate category
279    pub fn classify_fhir_structure_def(
280        &self,
281        structure_def: &StructureDefinition,
282    ) -> FhirTypeCategory {
283        let file_generator = FileGenerator::new(self.config, self.token_generator);
284        file_generator.classify_fhir_structure_def(structure_def)
285    }
286
287    /// Ensure the given directory exists, creating it if necessary
288    pub fn ensure_directory(dir_path: &Path) -> CodegenResult<()> {
289        std::fs::create_dir_all(dir_path)?;
290        Ok(())
291    }
292
293    /// Generate the appropriate file path for a structure definition
294    pub fn get_output_path_for_structure<P: AsRef<Path>>(
295        base_dir: P,
296        structure_def: &StructureDefinition,
297        file_generator: &FileGenerator,
298    ) -> std::path::PathBuf {
299        let base_dir = base_dir.as_ref();
300
301        // Determine the appropriate subdirectory based on FHIR type
302        let target_dir = match file_generator.classify_fhir_structure_def(structure_def) {
303            FhirTypeCategory::Resource => base_dir.join("src").join("resources"),
304            FhirTypeCategory::Profile => base_dir.join("src").join("profiles"),
305            FhirTypeCategory::DataType => base_dir.join("src").join("datatypes"),
306            FhirTypeCategory::Extension => base_dir.join("src").join("extensions"),
307            FhirTypeCategory::Primitive => base_dir.join("src").join("primitives"),
308        };
309
310        let filename = crate::naming::Naming::filename(structure_def);
311        target_dir.join(filename)
312    }
313
314    /// Generate the traits directory path
315    pub fn get_traits_directory_path<P: AsRef<Path>>(base_dir: P) -> std::path::PathBuf {
316        base_dir.as_ref().join("src").join("traits")
317    }
318
319    /// Generate the trait file path for a structure definition
320    pub fn get_trait_file_path<P: AsRef<Path>>(
321        base_dir: P,
322        structure_def: &StructureDefinition,
323    ) -> std::path::PathBuf {
324        let traits_dir = Self::get_traits_directory_path(base_dir);
325        let struct_name = crate::naming::Naming::struct_name(structure_def);
326        let snake_case_name = crate::naming::Naming::to_snake_case(&struct_name);
327        let filename = format!("{snake_case_name}.rs");
328        traits_dir.join(filename)
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::config::CodegenConfig;
336    use crate::generators::TokenGenerator;
337    use tempfile::TempDir;
338
339    #[test]
340    fn test_load_structure_definition() {
341        use std::fs;
342
343        let temp_dir = TempDir::new().unwrap();
344        let file_path = temp_dir.path().join("test_structure.json");
345
346        // Create a minimal valid StructureDefinition JSON manually
347        let json_content = r#"{
348            "resourceType": "StructureDefinition",
349            "id": "Patient",
350            "url": "http://hl7.org/fhir/StructureDefinition/Patient",
351            "name": "Patient",
352            "title": "Patient",
353            "status": "active",
354            "kind": "resource",
355            "abstract": false,
356            "type": "Patient",
357            "description": "A patient resource",
358            "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
359        }"#;
360
361        fs::write(&file_path, json_content).unwrap();
362
363        // Test loading
364        let result = FileIoManager::load_structure_definition(&file_path);
365        assert!(
366            result.is_ok(),
367            "Should load StructureDefinition successfully"
368        );
369
370        let loaded_structure = result.unwrap();
371        assert_eq!(loaded_structure.name, "Patient");
372        assert_eq!(loaded_structure.kind, "resource");
373    }
374
375    #[test]
376    fn test_load_structure_definition_invalid_json() {
377        let temp_dir = TempDir::new().unwrap();
378        let file_path = temp_dir.path().join("invalid.json");
379
380        // Write invalid JSON
381        fs::write(&file_path, "{ invalid json }").unwrap();
382
383        let result = FileIoManager::load_structure_definition(&file_path);
384        assert!(result.is_err(), "Should fail to load invalid JSON");
385    }
386
387    #[test]
388    fn test_load_structure_definition_missing_file() {
389        let temp_dir = TempDir::new().unwrap();
390        let file_path = temp_dir.path().join("missing.json");
391
392        let result = FileIoManager::load_structure_definition(&file_path);
393        assert!(result.is_err(), "Should fail to load missing file");
394    }
395
396    #[test]
397    fn test_collect_nested_structs() {
398        let mut type_cache = HashMap::new();
399
400        // Add main struct
401        let patient_struct = RustStruct::new("Patient".to_string());
402        type_cache.insert("Patient".to_string(), patient_struct);
403
404        // Add nested structs
405        let patient_contact_struct = RustStruct::new("PatientContact".to_string());
406        type_cache.insert("PatientContact".to_string(), patient_contact_struct);
407
408        let patient_link_struct = RustStruct::new("PatientLink".to_string());
409        type_cache.insert("PatientLink".to_string(), patient_link_struct);
410
411        // Add unrelated struct
412        let observation_struct = RustStruct::new("Observation".to_string());
413        type_cache.insert("Observation".to_string(), observation_struct);
414
415        let nested_structs = FileIoManager::collect_nested_structs("Patient", &type_cache);
416
417        assert_eq!(
418            nested_structs.len(),
419            2,
420            "Should collect exactly 2 nested structs"
421        );
422
423        let nested_names: Vec<String> = nested_structs.iter().map(|s| s.name.clone()).collect();
424        assert!(nested_names.contains(&"PatientContact".to_string()));
425        assert!(nested_names.contains(&"PatientLink".to_string()));
426        assert!(!nested_names.contains(&"Patient".to_string())); // Shouldn't include main struct
427        assert!(!nested_names.contains(&"Observation".to_string())); // Shouldn't include unrelated struct
428    }
429
430    #[test]
431    fn test_ensure_directory_exists() {
432        let temp_dir = TempDir::new().unwrap();
433        let new_dir = temp_dir.path().join("nested").join("directory");
434
435        // Directory shouldn't exist initially
436        assert!(!new_dir.exists());
437
438        let result = FileIoManager::ensure_directory(&new_dir);
439        assert!(result.is_ok(), "Should create directory successfully");
440
441        // Directory should now exist
442        assert!(new_dir.exists());
443        assert!(new_dir.is_dir());
444    }
445
446    #[test]
447    fn test_get_output_path_for_structure() {
448        let config = CodegenConfig::default();
449        let token_generator = TokenGenerator::new();
450        let file_generator = FileGenerator::new(&config, &token_generator);
451
452        let temp_dir = TempDir::new().unwrap();
453
454        // Create test JSON content manually
455        let json_content = r#"{
456            "resourceType": "StructureDefinition",
457            "id": "Patient",
458            "url": "http://hl7.org/fhir/StructureDefinition/Patient",
459            "name": "Patient",
460            "title": "Patient",
461            "status": "active",
462            "kind": "resource",
463            "abstract": false,
464            "type": "Patient",
465            "description": "A patient resource",
466            "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
467        }"#;
468
469        // Parse the structure definition
470        let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
471
472        let output_path = FileIoManager::get_output_path_for_structure(
473            temp_dir.path(),
474            &patient_structure,
475            &file_generator,
476        );
477
478        let expected_path = temp_dir
479            .path()
480            .join("src")
481            .join("resources")
482            .join("patient.rs");
483        assert_eq!(output_path, expected_path);
484    }
485
486    #[test]
487    fn test_get_trait_file_path() {
488        let temp_dir = TempDir::new().unwrap();
489
490        // Create test JSON content manually
491        let json_content = r#"{
492            "resourceType": "StructureDefinition",
493            "id": "Patient",
494            "url": "http://hl7.org/fhir/StructureDefinition/Patient",
495            "name": "Patient",
496            "title": "Patient",
497            "status": "active",
498            "kind": "resource",
499            "abstract": false,
500            "type": "Patient",
501            "description": "A patient resource",
502            "baseDefinition": "http://hl7.org/fhir/StructureDefinition/DomainResource"
503        }"#;
504
505        // Parse the structure definition
506        let patient_structure: StructureDefinition = serde_json::from_str(json_content).unwrap();
507
508        let trait_path = FileIoManager::get_trait_file_path(temp_dir.path(), &patient_structure);
509        let expected_path = temp_dir
510            .path()
511            .join("src")
512            .join("traits")
513            .join("patient.rs");
514        assert_eq!(trait_path, expected_path);
515    }
516
517    #[test]
518    fn test_file_io_manager_creation() {
519        let config = CodegenConfig::default();
520        let token_generator = TokenGenerator::new();
521        let file_io_manager = FileIoManager::new(&config, &token_generator);
522
523        // Test that the manager can be created successfully
524        // This is mainly to ensure the lifetime parameters work correctly
525        // If this compiles and runs, the test passes
526        assert_eq!(
527            std::mem::size_of_val(&file_io_manager),
528            std::mem::size_of::<FileIoManager>()
529        );
530    }
531
532    #[test]
533    fn test_nested_struct_collection_fix() {
534        use crate::rust_types::RustStruct;
535        use std::collections::HashMap;
536
537        let mut type_cache = HashMap::new();
538
539        // Create main structs
540        let element_struct = RustStruct::new("Element".to_string());
541        let element_definition_struct = RustStruct::new("ElementDefinition".to_string());
542
543        // Create nested structs for Element
544        let element_extension_struct = RustStruct::new("ElementExtension".to_string());
545        let element_binding_struct = RustStruct::new("ElementBinding".to_string());
546
547        // Create nested structs for ElementDefinition (these should NOT be collected for Element)
548        let element_definition_binding_struct =
549            RustStruct::new("ElementDefinitionBinding".to_string());
550        let element_definition_constraint_struct =
551            RustStruct::new("ElementDefinitionConstraint".to_string());
552        let element_definition_type_struct = RustStruct::new("ElementDefinitionType".to_string());
553
554        // Add all structs to cache
555        type_cache.insert("Element".to_string(), element_struct);
556        type_cache.insert("ElementDefinition".to_string(), element_definition_struct);
557        type_cache.insert("ElementExtension".to_string(), element_extension_struct);
558        type_cache.insert("ElementBinding".to_string(), element_binding_struct);
559        type_cache.insert(
560            "ElementDefinitionBinding".to_string(),
561            element_definition_binding_struct,
562        );
563        type_cache.insert(
564            "ElementDefinitionConstraint".to_string(),
565            element_definition_constraint_struct,
566        );
567        type_cache.insert(
568            "ElementDefinitionType".to_string(),
569            element_definition_type_struct,
570        );
571
572        // Add lowercase version of ElementDefinition structs (as they appear in real FHIR data)
573        let elementdefinition_binding_struct =
574            RustStruct::new("ElementdefinitionBinding".to_string());
575        type_cache.insert(
576            "ElementdefinitionBinding".to_string(),
577            elementdefinition_binding_struct,
578        );
579        let elementdefinition_constraint_struct =
580            RustStruct::new("ElementdefinitionConstraint".to_string());
581        type_cache.insert(
582            "ElementdefinitionConstraint".to_string(),
583            elementdefinition_constraint_struct,
584        );
585        let elementdefinition_type_struct = RustStruct::new("ElementdefinitionType".to_string());
586        type_cache.insert(
587            "ElementdefinitionType".to_string(),
588            elementdefinition_type_struct,
589        );
590
591        // Test collecting nested structs for Element
592        let element_nested = FileIoManager::collect_nested_structs("Element", &type_cache);
593
594        // Element should only collect ElementExtension and ElementBinding,
595        // NOT ElementDefinition* structs
596        assert_eq!(
597            element_nested.len(),
598            2,
599            "Element should have 2 nested structs"
600        );
601
602        let element_nested_names: Vec<String> =
603            element_nested.iter().map(|s| s.name.clone()).collect();
604        assert!(element_nested_names.contains(&"ElementExtension".to_string()));
605        assert!(element_nested_names.contains(&"ElementBinding".to_string()));
606        assert!(!element_nested_names.contains(&"ElementDefinitionBinding".to_string()));
607        assert!(!element_nested_names.contains(&"ElementDefinitionConstraint".to_string()));
608        assert!(!element_nested_names.contains(&"ElementDefinitionType".to_string()));
609        assert!(!element_nested_names.contains(&"ElementDefinition".to_string()));
610        // Test that lowercase versions are also excluded
611        assert!(!element_nested_names.contains(&"ElementdefinitionBinding".to_string()));
612        assert!(!element_nested_names.contains(&"ElementdefinitionConstraint".to_string()));
613        assert!(!element_nested_names.contains(&"ElementdefinitionType".to_string()));
614
615        // Test collecting nested structs for ElementDefinition
616        let element_definition_nested =
617            FileIoManager::collect_nested_structs("ElementDefinition", &type_cache);
618
619        // ElementDefinition should collect all ElementDefinition* structs (uppercase),
620        // but NOT Elementdefinition* structs (lowercase) because they are separate entities
621        assert_eq!(
622            element_definition_nested.len(),
623            3,
624            "ElementDefinition should have 3 nested structs (only uppercase)"
625        );
626
627        let element_definition_nested_names: Vec<String> = element_definition_nested
628            .iter()
629            .map(|s| s.name.clone())
630            .collect();
631        assert!(element_definition_nested_names.contains(&"ElementDefinitionBinding".to_string()));
632        assert!(
633            element_definition_nested_names.contains(&"ElementDefinitionConstraint".to_string())
634        );
635        assert!(element_definition_nested_names.contains(&"ElementDefinitionType".to_string()));
636        // Test that lowercase versions are NOT collected by ElementDefinition because they are separate entities
637        assert!(!element_definition_nested_names.contains(&"ElementdefinitionBinding".to_string()));
638        assert!(
639            !element_definition_nested_names.contains(&"ElementdefinitionConstraint".to_string())
640        );
641        assert!(!element_definition_nested_names.contains(&"ElementdefinitionType".to_string()));
642        assert!(!element_definition_nested_names.contains(&"ElementExtension".to_string()));
643        assert!(!element_definition_nested_names.contains(&"ElementBinding".to_string()));
644
645        // Test some edge cases to ensure the logic is robust
646
647        // Test that Bundle correctly collects BundleEntry but not BundleDefinition (if it existed)
648        type_cache.insert("Bundle".to_string(), RustStruct::new("Bundle".to_string()));
649        type_cache.insert(
650            "BundleEntry".to_string(),
651            RustStruct::new("BundleEntry".to_string()),
652        );
653        type_cache.insert(
654            "BundleLink".to_string(),
655            RustStruct::new("BundleLink".to_string()),
656        );
657
658        let bundle_nested = FileIoManager::collect_nested_structs("Bundle", &type_cache);
659        assert_eq!(
660            bundle_nested.len(),
661            2,
662            "Bundle should have 2 nested structs"
663        );
664
665        let bundle_nested_names: Vec<String> =
666            bundle_nested.iter().map(|s| s.name.clone()).collect();
667        assert!(bundle_nested_names.contains(&"BundleEntry".to_string()));
668        assert!(bundle_nested_names.contains(&"BundleLink".to_string()));
669    }
670}