Skip to main content

rh_codegen/generators/
struct_generator.rs

1//! Struct generation functionality
2//!
3//! This module handles the generation of Rust structs from FHIR StructureDefinitions.
4
5use std::collections::HashMap;
6
7use crate::config::CodegenConfig;
8use crate::fhir_types::StructureDefinition;
9use crate::generators::{DocumentationGenerator, FieldGenerator, TypeUtilities};
10use crate::rust_types::{RustField, RustStruct, RustType};
11use crate::value_sets::ValueSetManager;
12use crate::{CodegenError, CodegenResult};
13
14/// Struct generator for FHIR StructureDefinitions
15pub struct StructGenerator<'a> {
16    config: &'a CodegenConfig,
17    type_cache: &'a mut HashMap<String, RustStruct>,
18    value_set_manager: &'a mut ValueSetManager,
19}
20
21impl<'a> StructGenerator<'a> {
22    /// Create a new struct generator
23    pub fn new(
24        config: &'a CodegenConfig,
25        type_cache: &'a mut HashMap<String, RustStruct>,
26        value_set_manager: &'a mut ValueSetManager,
27    ) -> Self {
28        Self {
29            config,
30            type_cache,
31            value_set_manager,
32        }
33    }
34
35    /// Generate a Rust struct from a FHIR StructureDefinition
36    pub fn generate_struct(
37        &mut self,
38        structure_def: &StructureDefinition,
39    ) -> CodegenResult<RustStruct> {
40        // Skip LogicalModels as they are conceptual models, not implementable types
41        if structure_def.kind == "logical" {
42            return Err(CodegenError::Generation {
43                message: format!(
44                    "Skipping LogicalModel '{}' - logical models are not generated as Rust types",
45                    structure_def.name
46                ),
47            });
48        }
49
50        // Skip examples
51        if structure_def.url.to_lowercase().contains("example") {
52            return Err(CodegenError::Generation {
53                message: format!(
54                    "Skipping example StructureDefinition '{}'",
55                    structure_def.name
56                ),
57            });
58        }
59
60        // Skip files that begin with underscore (auto-generated/temporary files)
61        if TypeUtilities::should_skip_underscore_prefixed(structure_def) {
62            return Err(CodegenError::Generation {
63                message: format!(
64                    "Skipping underscore-prefixed StructureDefinition '{}' - underscore prefixed files are ignored",
65                    structure_def.name
66                ),
67            });
68        }
69
70        // Generate struct name from URL or ID, not the name field
71        let struct_name = crate::naming::Naming::struct_name(structure_def);
72
73        // Check if we've already generated this type
74        if let Some(cached_struct) = self.type_cache.get(&struct_name) {
75            return Ok(cached_struct.clone());
76        }
77
78        // Create the struct with enhanced documentation
79        let mut rust_struct = RustStruct::new(struct_name.clone());
80        rust_struct.doc_comment =
81            DocumentationGenerator::generate_struct_documentation(structure_def);
82
83        // Use config to determine derives
84        let mut derives = vec!["Debug".to_string(), "Clone".to_string()];
85        if self.config.with_serde {
86            derives.extend(vec!["Serialize".to_string(), "Deserialize".to_string()]);
87        }
88
89        // Add Default derive for profiles since they only have a base field that implements Default
90        // Check using TypeRegistry to see if this is a profile
91        let is_profile = crate::generators::type_registry::TypeRegistry::is_profile(structure_def);
92        if is_profile {
93            derives.push("Default".to_string());
94        }
95
96        rust_struct.derives = derives;
97
98        // Set the base definition for inheritance if present
99        if let Some(base_def) = &structure_def.base_definition {
100            rust_struct.base_definition = Some(
101                base_def
102                    .split('/')
103                    .next_back()
104                    .unwrap_or(base_def)
105                    .to_string(),
106            );
107        }
108
109        // Handle primitive types specially
110        if structure_def.kind == "primitive-type" {
111            return self.generate_primitive_type_struct(structure_def, rust_struct);
112        }
113
114        // Extract element definitions from differential only (preferred FHIR approach)
115        let elements = if let Some(differential) = &structure_def.differential {
116            &differential.element
117        } else if let Some(snapshot) = &structure_def.snapshot {
118            &snapshot.element
119        } else {
120            return Ok(rust_struct); // No elements to process
121        };
122
123        // Process each element definition to create struct fields and nested structs
124        let mut nested_structs_info = HashMap::new();
125        let mut direct_fields = Vec::new();
126
127        for element in elements {
128            // Skip the root element
129            if element.path == structure_def.name || element.path == structure_def.base_type {
130                continue;
131            }
132
133            // Only process elements that belong to this struct
134            let base_path = &structure_def.name;
135            if !element.path.starts_with(&format!("{base_path}.")) {
136                continue;
137            }
138
139            let field_path = element.path.strip_prefix(&format!("{base_path}.")).unwrap();
140
141            if field_path.contains('.') {
142                // This is a nested field - collect it for nested struct generation
143                let nested_field_name = field_path.split('.').next().unwrap();
144                nested_structs_info
145                    .entry(nested_field_name.to_string())
146                    .or_insert_with(Vec::new)
147                    .push(element.clone());
148            } else {
149                // This is a direct field of this struct
150                direct_fields.push(element.clone());
151            }
152        }
153
154        // First pass: Generate nested structs for BackboneElements
155        for (nested_field_name, nested_elements) in &nested_structs_info {
156            if let Some(nested_struct) = self.generate_nested_struct(
157                &struct_name,
158                nested_field_name,
159                nested_elements,
160                structure_def,
161            )? {
162                // Store the nested struct in cache for later use
163                self.type_cache
164                    .insert(nested_struct.name.clone(), nested_struct.clone());
165
166                // Register the nested struct in TypeRegistry with proper classification
167                crate::generators::type_registry::TypeRegistry::register_type_classification_only(
168                    &nested_struct.name,
169                    crate::generators::type_registry::TypeClassification::NestedStructure {
170                        parent_resource: struct_name.clone(),
171                    },
172                );
173            }
174        }
175
176        // Second pass: Process direct fields (now nested structs are available)
177        for element in direct_fields {
178            let fields = self.create_fields_from_element(&element)?;
179            for mut field in fields {
180                // Apply Box wrapper for circular dependencies
181                field.field_type =
182                    Self::apply_box_for_circular_dependencies(field.field_type, &rust_struct.name);
183                rust_struct.add_field(field);
184            }
185        }
186
187        // Cache the generated struct for future use
188        self.type_cache.insert(struct_name, rust_struct.clone());
189
190        Ok(rust_struct)
191    }
192
193    /// Generate a primitive type struct with special FHIR primitive type semantics
194    pub fn generate_primitive_type_struct(
195        &mut self,
196        structure_def: &StructureDefinition,
197        mut rust_struct: RustStruct,
198    ) -> CodegenResult<RustStruct> {
199        // For primitive types, don't inherit from Element
200        rust_struct.base_definition = None;
201
202        // Map FHIR primitive types to Rust types
203        let rust_primitive_type = match structure_def.name.as_str() {
204            "boolean" => RustType::Boolean,
205            "integer" | "positiveInt" | "unsignedInt" => RustType::Integer,
206            "decimal" => RustType::Float,
207            "string" | "code" | "id" | "markdown" | "uri" | "url" | "canonical" | "oid"
208            | "uuid" | "base64Binary" | "xhtml" => RustType::String,
209            "date" | "dateTime" | "time" | "instant" => RustType::String, // Could use chrono types later
210            _ => RustType::String, // Default to String for unknown primitive types
211        };
212
213        // The primitive type is just a type alias or newtype wrapper around the Rust primitive
214        // For now, we'll create a struct with a single `value` field
215        let value_field = RustField::new("value".to_string(), rust_primitive_type);
216        rust_struct.add_field(value_field);
217
218        // Cache the generated struct for future use
219        let struct_name = rust_struct.name.clone();
220        self.type_cache.insert(struct_name, rust_struct.clone());
221
222        Ok(rust_struct)
223    }
224
225    /// Generate a nested struct for BackboneElements
226    pub fn generate_nested_struct(
227        &mut self,
228        parent_struct_name: &str,
229        nested_field_name: &str,
230        nested_elements: &[crate::fhir_types::ElementDefinition],
231        parent_structure_def: &StructureDefinition,
232    ) -> CodegenResult<Option<RustStruct>> {
233        // Generate the nested struct name (e.g., BundleEntry, BundleLink)
234        let nested_struct_name = format!(
235            "{}{}",
236            parent_struct_name,
237            crate::naming::Naming::to_pascal_case(nested_field_name)
238        );
239
240        // Check if we've already generated this nested struct
241        if self.type_cache.contains_key(&nested_struct_name) {
242            return Ok(None);
243        }
244
245        // Create the nested struct
246        let mut nested_struct = RustStruct::new(nested_struct_name.clone());
247
248        // Add documentation
249        nested_struct.doc_comment = Some(
250            DocumentationGenerator::generate_nested_struct_documentation(
251                parent_struct_name,
252                nested_field_name,
253            ),
254        );
255
256        // Use config to determine derives
257        let mut derives = vec!["Debug".to_string(), "Clone".to_string()];
258        if self.config.with_serde {
259            derives.extend(vec!["Serialize".to_string(), "Deserialize".to_string()]);
260        }
261        nested_struct.derives = derives;
262
263        // Set base as BackboneElement since these are typically BackboneElements
264        nested_struct.base_definition = Some("BackboneElement".to_string());
265
266        // Process the nested elements
267        let base_path = format!("{}.{}", parent_structure_def.name, nested_field_name);
268        let mut sub_nested_structs = HashMap::new();
269        let mut direct_fields = Vec::new();
270
271        for element in nested_elements {
272            if !element.path.starts_with(&base_path) {
273                continue;
274            }
275
276            // Skip the BackboneElement definition itself (where path == base_path)
277            if element.path == base_path {
278                continue;
279            }
280
281            let field_path = element.path.strip_prefix(&format!("{base_path}.")).unwrap();
282
283            if field_path.contains('.') {
284                // This is a sub-nested field - collect it for recursive nested struct generation
285                let sub_nested_field_name = field_path.split('.').next().unwrap();
286                sub_nested_structs
287                    .entry(sub_nested_field_name.to_string())
288                    .or_insert_with(Vec::new)
289                    .push(element.clone());
290            } else {
291                // Check if this direct field is itself a BackboneElement that needs a nested struct
292                let is_backbone_element = element
293                    .element_type
294                    .as_ref()
295                    .and_then(|types| types.first())
296                    .and_then(|t| t.code.as_ref())
297                    .map(|code| code == "BackboneElement")
298                    .unwrap_or(false);
299
300                if is_backbone_element {
301                    // Treat this as a sub-nested struct even though it appears as a direct field
302                    sub_nested_structs
303                        .entry(field_path.to_string())
304                        .or_insert_with(Vec::new)
305                        .push(element.clone());
306                } else {
307                    // This is a direct field of this nested struct
308                    direct_fields.push(element.clone());
309                }
310            }
311        }
312
313        // First, generate any sub-nested structs
314        for (sub_nested_field_name, sub_nested_elements) in &sub_nested_structs {
315            // For recursive calls, we need to create a modified context
316            // The base path for sub-nested structs should be the current nested struct's path
317            let sub_nested_struct_name = format!(
318                "{}{}",
319                nested_struct_name,
320                crate::naming::Naming::to_pascal_case(sub_nested_field_name)
321            );
322
323            if !self.type_cache.contains_key(&sub_nested_struct_name) {
324                let mut sub_nested_struct = RustStruct::new(sub_nested_struct_name.clone());
325
326                sub_nested_struct.doc_comment = Some(
327                    DocumentationGenerator::generate_sub_nested_struct_documentation(
328                        &nested_struct_name,
329                        sub_nested_field_name,
330                    ),
331                );
332
333                // Use config to determine derives
334                let mut derives = vec!["Debug".to_string(), "Clone".to_string()];
335                if self.config.with_serde {
336                    derives.extend(vec!["Serialize".to_string(), "Deserialize".to_string()]);
337                }
338                sub_nested_struct.derives = derives;
339                sub_nested_struct.base_definition = Some("BackboneElement".to_string());
340
341                // Process the sub-nested elements with full recursive support
342                let sub_base_path = format!("{base_path}.{sub_nested_field_name}");
343
344                // Separate sub-nested elements into direct fields and further sub-nested structures
345                let mut sub_direct_fields = Vec::new();
346                let mut sub_sub_nested_structs: HashMap<
347                    String,
348                    Vec<crate::fhir_types::ElementDefinition>,
349                > = HashMap::new();
350
351                for element in sub_nested_elements {
352                    if !element.path.starts_with(&sub_base_path) {
353                        continue;
354                    }
355
356                    // Skip the BackboneElement definition itself (where path == base_path)
357                    if element.path == sub_base_path {
358                        continue;
359                    }
360
361                    let sub_field_path = element
362                        .path
363                        .strip_prefix(&format!("{sub_base_path}."))
364                        .unwrap();
365
366                    if sub_field_path.contains('.') {
367                        // This is a further sub-nested field - collect it for recursive generation
368                        let sub_sub_nested_field_name = sub_field_path.split('.').next().unwrap();
369                        sub_sub_nested_structs
370                            .entry(sub_sub_nested_field_name.to_string())
371                            .or_default()
372                            .push(element.clone());
373                    } else {
374                        // Check if this direct field is itself a BackboneElement that needs a nested struct
375                        let is_backbone_element = element
376                            .element_type
377                            .as_ref()
378                            .and_then(|types| types.first())
379                            .and_then(|t| t.code.as_ref())
380                            .map(|code| code == "BackboneElement")
381                            .unwrap_or(false);
382
383                        if is_backbone_element {
384                            // Treat this as a further sub-nested struct even though it appears as a direct field
385                            sub_sub_nested_structs
386                                .entry(sub_field_path.to_string())
387                                .or_default()
388                                .push(element.clone());
389                        } else {
390                            // This is a direct field of this sub-nested struct
391                            sub_direct_fields.push(element.clone());
392                        }
393                    }
394                }
395
396                // First, recursively generate any further sub-nested structs
397                for (sub_sub_nested_field_name, sub_sub_nested_elements) in &sub_sub_nested_structs
398                {
399                    self.generate_deeply_nested_struct(
400                        &sub_nested_struct_name,
401                        sub_sub_nested_field_name,
402                        sub_sub_nested_elements,
403                        &sub_base_path,
404                    )?;
405                }
406
407                // Then, process direct fields (now further sub-nested structs are available)
408                for element in sub_direct_fields {
409                    let fields = self.create_fields_from_element(&element)?;
410                    for field in fields {
411                        sub_nested_struct.add_field(field);
412                    }
413                }
414
415                // Store the sub-nested struct in cache
416                self.type_cache
417                    .insert(sub_nested_struct_name.clone(), sub_nested_struct);
418
419                // Register the sub-nested struct in TypeRegistry with proper classification
420                // Use the parent struct name (first level nested struct's parent is the resource)
421                crate::generators::type_registry::TypeRegistry::register_type_classification_only(
422                    &sub_nested_struct_name,
423                    crate::generators::type_registry::TypeClassification::NestedStructure {
424                        parent_resource: parent_struct_name.to_string(),
425                    },
426                );
427            }
428
429            // Now create a field in the parent struct that references this sub-nested struct
430            // Find the BackboneElement definition for this field
431            let backbone_element_def = sub_nested_elements
432                .iter()
433                .find(|e| e.path == format!("{base_path}.{sub_nested_field_name}"));
434
435            if let Some(element) = backbone_element_def {
436                let fields = self.create_fields_from_element(element)?;
437                for field in fields {
438                    nested_struct.add_field(field);
439                }
440            }
441        }
442
443        // Then, process direct fields (now sub-nested structs are available)
444        for element in direct_fields {
445            let fields = self.create_fields_from_element(&element)?;
446            for field in fields {
447                nested_struct.add_field(field);
448            }
449        }
450
451        Ok(Some(nested_struct))
452    }
453
454    /// Recursively generate deeply nested structs (for arbitrary nesting levels)
455    fn generate_deeply_nested_struct(
456        &mut self,
457        parent_nested_struct_name: &str,
458        field_name: &str,
459        elements: &[crate::fhir_types::ElementDefinition],
460        parent_base_path: &str,
461    ) -> CodegenResult<()> {
462        let nested_struct_name = format!(
463            "{}{}",
464            parent_nested_struct_name,
465            crate::naming::Naming::to_pascal_case(field_name)
466        );
467
468        if !self.type_cache.contains_key(&nested_struct_name) {
469            let mut nested_struct = RustStruct::new(nested_struct_name.clone());
470
471            nested_struct.doc_comment = Some(
472                DocumentationGenerator::generate_sub_nested_struct_documentation(
473                    parent_nested_struct_name,
474                    field_name,
475                ),
476            );
477
478            // Use config to determine derives
479            let mut derives = vec!["Debug".to_string(), "Clone".to_string()];
480            if self.config.with_serde {
481                derives.extend(vec!["Serialize".to_string(), "Deserialize".to_string()]);
482            }
483            nested_struct.derives = derives;
484            nested_struct.base_definition = Some("BackboneElement".to_string());
485
486            // Process the nested elements with full recursive support
487            let base_path = format!("{parent_base_path}.{field_name}");
488
489            // Separate nested elements into direct fields and further nested structures
490            let mut direct_fields = Vec::new();
491            let mut sub_nested_structs: HashMap<String, Vec<crate::fhir_types::ElementDefinition>> =
492                HashMap::new();
493
494            for element in elements {
495                if !element.path.starts_with(&base_path) {
496                    continue;
497                }
498
499                // Skip the BackboneElement definition itself (where path == base_path)
500                if element.path == base_path {
501                    continue;
502                }
503
504                let field_path = element.path.strip_prefix(&format!("{base_path}.")).unwrap();
505
506                if field_path.contains('.') {
507                    // This is a further nested field - collect it for recursive generation
508                    let sub_nested_field_name = field_path.split('.').next().unwrap();
509                    sub_nested_structs
510                        .entry(sub_nested_field_name.to_string())
511                        .or_default()
512                        .push(element.clone());
513                } else {
514                    // Check if this direct field is itself a BackboneElement that needs a nested struct
515                    let is_backbone_element = element
516                        .element_type
517                        .as_ref()
518                        .and_then(|types| types.first())
519                        .and_then(|t| t.code.as_ref())
520                        .map(|code| code == "BackboneElement")
521                        .unwrap_or(false);
522
523                    if is_backbone_element {
524                        // Treat this as a further nested struct even though it appears as a direct field
525                        sub_nested_structs
526                            .entry(field_path.to_string())
527                            .or_default()
528                            .push(element.clone());
529                    } else {
530                        // This is a direct field of this nested struct
531                        direct_fields.push(element.clone());
532                    }
533                }
534            }
535
536            // First, recursively generate any further nested structs
537            for (sub_nested_field_name, sub_nested_elements) in &sub_nested_structs {
538                self.generate_deeply_nested_struct(
539                    &nested_struct_name,
540                    sub_nested_field_name,
541                    sub_nested_elements,
542                    &base_path,
543                )?;
544            }
545
546            // Then, process direct fields (now further nested structs are available)
547            for element in direct_fields {
548                let fields = self.create_fields_from_element(&element)?;
549                for field in fields {
550                    nested_struct.add_field(field);
551                }
552            }
553
554            // Store the nested struct in cache
555            self.type_cache
556                .insert(nested_struct_name.clone(), nested_struct);
557
558            // Register the deeply nested struct in TypeRegistry with proper classification
559            // Extract the root parent resource name (e.g., "MedicationKnowledge" from "MedicationKnowledgeAdministrationguidelinesDosage")
560            let root_parent_resource = Self::extract_root_parent_resource(&nested_struct_name);
561            crate::generators::type_registry::TypeRegistry::register_type_classification_only(
562                &nested_struct_name,
563                crate::generators::type_registry::TypeClassification::NestedStructure {
564                    parent_resource: root_parent_resource,
565                },
566            );
567        }
568
569        Ok(())
570    }
571
572    /// Extract the root parent resource name from a deeply nested struct name
573    /// For example: "MedicationKnowledgeAdministrationguidelinesDosage" -> "MedicationKnowledge"
574    fn extract_root_parent_resource(nested_struct_name: &str) -> String {
575        // Use the TypeRegistry's method for consistency
576        crate::generators::type_registry::TypeRegistry::extract_parent_from_name(nested_struct_name)
577            .unwrap_or_else(|| nested_struct_name.to_string())
578    }
579
580    /// Check if a field should use a nested struct type instead of BackboneElement
581    pub fn should_use_nested_struct_type(
582        &self,
583        element: &crate::fhir_types::ElementDefinition,
584        element_types: &[crate::fhir_types::ElementType],
585    ) -> bool {
586        // Check if this element is a BackboneElement and we have nested elements for it
587        if let Some(first_type) = element_types.first() {
588            if let Some(code) = &first_type.code {
589                if code == "BackboneElement" {
590                    // Extract parent struct name and field name from the path
591                    let path_parts: Vec<&str> = element.path.split('.').collect();
592                    if path_parts.len() >= 2 {
593                        let _parent_name = path_parts[0];
594                        let _field_name = path_parts[1];
595                        // We would check here if we have nested elements for this field
596                        // For now, always return true for BackboneElements
597                        return true;
598                    }
599                }
600            }
601        }
602        false
603    }
604
605    /// Create a RustField from an ElementDefinition
606    pub fn create_field_from_element(
607        &mut self,
608        element: &crate::fhir_types::ElementDefinition,
609    ) -> CodegenResult<Option<RustField>> {
610        let mut field_generator =
611            FieldGenerator::new(self.config, self.type_cache, self.value_set_manager);
612        field_generator.create_field_from_element(element)
613    }
614
615    /// Create RustField(s) from an ElementDefinition (supports choice types)
616    pub fn create_fields_from_element(
617        &mut self,
618        element: &crate::fhir_types::ElementDefinition,
619    ) -> CodegenResult<Vec<RustField>> {
620        let mut field_generator =
621            FieldGenerator::new(self.config, self.type_cache, self.value_set_manager);
622        field_generator.create_fields_from_element(element)
623    }
624
625    /// Apply Box wrapper to field types to prevent circular dependencies
626    fn apply_box_for_circular_dependencies(
627        field_type: RustType,
628        current_struct_name: &str,
629    ) -> RustType {
630        // Known circular dependency pairs that need Box wrapping
631        let circular_dependencies = [("Identifier", "Reference"), ("Reference", "Identifier")];
632
633        // Check if we need to wrap this type in Box
634        match &field_type {
635            RustType::Custom(type_name) => {
636                for (struct_a, struct_b) in &circular_dependencies {
637                    if (current_struct_name == *struct_a && type_name == *struct_b)
638                        || (current_struct_name == *struct_b && type_name == *struct_a)
639                    {
640                        return RustType::Box(Box::new(field_type));
641                    }
642                }
643                field_type
644            }
645            RustType::Option(inner) => {
646                let boxed_inner = Self::apply_box_for_circular_dependencies(
647                    (**inner).clone(),
648                    current_struct_name,
649                );
650                if let RustType::Box(_) = boxed_inner {
651                    RustType::Option(Box::new(boxed_inner))
652                } else {
653                    field_type
654                }
655            }
656            RustType::Vec(inner) => {
657                let boxed_inner = Self::apply_box_for_circular_dependencies(
658                    (**inner).clone(),
659                    current_struct_name,
660                );
661                if let RustType::Box(_) = boxed_inner {
662                    RustType::Vec(Box::new(boxed_inner))
663                } else {
664                    field_type
665                }
666            }
667            _ => field_type,
668        }
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675    use crate::config::CodegenConfig;
676
677    #[test]
678    fn test_underscore_prefixed_structure_skipping() {
679        let config = CodegenConfig::default();
680        let mut type_cache = HashMap::new();
681        let mut value_set_manager = ValueSetManager::new();
682        let mut generator = StructGenerator::new(&config, &mut type_cache, &mut value_set_manager);
683
684        // Test structure with underscore prefixed name
685        let underscore_structure = StructureDefinition {
686            resource_type: "StructureDefinition".to_string(),
687            id: "normal-id".to_string(),
688            url: "http://hl7.org/fhir/StructureDefinition/_11179object_class".to_string(),
689            name: "_11179object_class".to_string(),
690            title: Some("Auto-generated class".to_string()),
691            status: "active".to_string(),
692            kind: "resource".to_string(),
693            is_abstract: false,
694            description: Some("An auto-generated resource".to_string()),
695            purpose: None,
696            base_type: "DomainResource".to_string(),
697            base_definition: Some(
698                "http://hl7.org/fhir/StructureDefinition/DomainResource".to_string(),
699            ),
700            version: None,
701            differential: None,
702            snapshot: None,
703        };
704
705        // Test that underscore-prefixed structures are rejected
706        let result = generator.generate_struct(&underscore_structure);
707        assert!(result.is_err());
708
709        if let Err(CodegenError::Generation { message }) = result {
710            assert!(message.contains("underscore prefixed files are ignored"));
711            assert!(message.contains("_11179object_class"));
712        } else {
713            panic!("Expected CodegenError::Generation for underscore-prefixed structure");
714        }
715    }
716}