Skip to main content

rh_codegen/generators/
accessor_trait_generator.rs

1//! Generator for accessor traits.
2use crate::fhir_types::{ElementDefinition, ElementType, StructureDefinition};
3use crate::rust_types::{RustTrait, RustTraitMethod, RustType};
4use crate::type_mapper::TypeMapper;
5use crate::value_sets::ValueSetManager;
6use crate::CodegenResult;
7
8#[derive(Default)]
9pub struct AccessorTraitGenerator {}
10
11impl AccessorTraitGenerator {
12    pub fn new() -> Self {
13        Self::default()
14    }
15
16    pub fn add_accessor_methods(
17        &self,
18        rust_trait: &mut RustTrait,
19        structure_def: &StructureDefinition,
20    ) -> CodegenResult<()> {
21        // Create TypeMapper for proper type resolution
22        let config = crate::config::CodegenConfig::default();
23        let mut value_set_manager = ValueSetManager::new();
24        let mut type_mapper = TypeMapper::new(&config, &mut value_set_manager);
25
26        let elements = structure_def
27            .differential
28            .as_ref()
29            .map_or(Vec::new(), |d| d.element.clone());
30
31        if elements.is_empty() {
32            if let Some(snapshot) = &structure_def.snapshot {
33                let snapshot_elements = snapshot.element.clone();
34                for element in &snapshot_elements {
35                    if self.should_generate_accessor(element, structure_def) {
36                        if let Some(method) =
37                            self.create_accessor_method(element, &mut type_mapper)?
38                        {
39                            rust_trait.add_method(method);
40                        }
41                    }
42                }
43            }
44        } else {
45            for element in &elements {
46                if self.should_generate_accessor(element, structure_def) {
47                    if let Some(method) = self.create_accessor_method(element, &mut type_mapper)? {
48                        rust_trait.add_method(method);
49                    }
50                }
51            }
52        }
53
54        self.add_choice_type_accessor_methods(rust_trait, structure_def)?;
55
56        // Add extension_by_url default helper for DomainResource and Element
57        if structure_def.name == "DomainResource" || structure_def.name == "Element" {
58            rust_trait.add_method(
59                RustTraitMethod::new("extension_by_url".to_string())
60                    .with_return_type(RustType::Custom("Option<&crate::datatypes::extension::Extension>".to_string()))
61                    .with_parameter("url".to_string(), RustType::Custom("&str".to_string()))
62                    .with_default_implementation(
63                        "self.extension().iter().find(|e| e.url == url)".to_string(),
64                    )
65                    .with_doc("Find an extension by its URL. Returns the first matching extension, or None if not found.".to_string()),
66            );
67        }
68
69        Ok(())
70    }
71
72    fn should_generate_accessor(
73        &self,
74        element: &ElementDefinition,
75        structure_def: &StructureDefinition,
76    ) -> bool {
77        let field_path = &element.path;
78        let base_name = &structure_def.name;
79
80        // The path must start with the base name of the structure.
81        if !field_path.starts_with(base_name) {
82            return false;
83        }
84
85        // We are interested in direct fields of the resource, which have paths like "Patient.active".
86        // Splitting by '.' should result in exactly two parts.
87        let path_parts: Vec<&str> = field_path.split('.').collect();
88        if path_parts.len() != 2 {
89            return false;
90        }
91
92        // The first part must match the base name.
93        if path_parts[0] != base_name {
94            return false;
95        }
96
97        // We don't generate accessors for choice types here, they are handled separately.
98        let field_name = path_parts[1];
99        !field_name.ends_with("[x]")
100    }
101
102    fn create_accessor_method(
103        &self,
104        element: &ElementDefinition,
105        type_mapper: &mut TypeMapper,
106    ) -> CodegenResult<Option<RustTraitMethod>> {
107        let path_parts: Vec<&str> = element.path.split('.').collect();
108        let field_name = path_parts
109            .last()
110            .ok_or_else(|| crate::CodegenError::MissingField {
111                field: format!("element path has no field segment: {}", element.path),
112            })?
113            .to_string();
114        let rust_field_name = crate::naming::Naming::field_name(&field_name);
115
116        let is_optional = element.min.unwrap_or(0) == 0;
117        let is_array = element.max.as_deref() == Some("*")
118            || element
119                .max
120                .as_deref()
121                .unwrap_or("1")
122                .parse::<i32>()
123                .unwrap_or(1)
124                > 1;
125
126        let Some(element_types) = element.element_type.as_ref() else {
127            return Ok(None);
128        };
129
130        // Check if this is a BackboneElement that should use a specific nested type
131        let rust_type = if self.is_backbone_element(element_types) {
132            self.get_nested_type_for_backbone_element(element, is_array)
133        } else if field_name == "language" && element.path.ends_with(".language") {
134            // Always treat `language` as a plain string — R5 adds a required binding to
135            // all-languages but keeping it as StringType stays consistent with the
136            // hardcoded trait-impl generator and with R4 behaviour.
137            if is_array {
138                RustType::Vec(Box::new(RustType::Custom("StringType".to_string())))
139            } else {
140                RustType::Custom("StringType".to_string())
141            }
142        } else {
143            // Use TypeMapper to get the correct type including enum bindings
144            let resolved = type_mapper.map_fhir_type_with_binding(
145                element_types,
146                element.binding.as_ref(),
147                is_array,
148            );
149            // Guard: if the resolved binding enum has the same name as the containing
150            // resource/type, fall back to StringType to avoid a self-referential type.
151            let resource_name = element.path.split('.').next().unwrap_or("");
152            let collides = matches!(&resolved, RustType::Custom(n) if n == resource_name);
153            if collides {
154                if is_array {
155                    RustType::Vec(Box::new(RustType::Custom("StringType".to_string())))
156                } else {
157                    RustType::Custom("StringType".to_string())
158                }
159            } else {
160                resolved
161            }
162        };
163
164        // Convert the type for trait return types
165        let return_type = match rust_type {
166            RustType::Custom(_) => {
167                // For enum types or custom types, keep as-is
168                // Don't convert StringType to &str to maintain compatibility with implementations
169                rust_type.clone()
170            }
171            RustType::Vec(inner) => RustType::Slice(inner),
172            other => other,
173        };
174
175        let method = RustTraitMethod::new(rust_field_name)
176            .with_doc(format!("Returns a reference to the {field_name} field."))
177            .with_return_type(if is_optional && !is_array {
178                return_type.clone().wrap_in_option()
179            } else {
180                return_type.clone()
181            })
182            .with_body(format!("self.{field_name}"));
183
184        Ok(Some(method))
185    }
186
187    /// Check if the element types contain BackboneElement
188    fn is_backbone_element(&self, element_types: &[ElementType]) -> bool {
189        element_types
190            .iter()
191            .any(|et| et.code.as_deref() == Some("BackboneElement"))
192    }
193
194    /// Get the specific nested type for a BackboneElement field
195    fn get_nested_type_for_backbone_element(
196        &self,
197        element: &ElementDefinition,
198        is_array: bool,
199    ) -> RustType {
200        let path_parts: Vec<&str> = element.path.split('.').collect();
201
202        if path_parts.len() == 2 {
203            let resource_name = path_parts[0];
204            let field_name = path_parts[1];
205
206            // Generate the expected nested type name: ResourceFieldName (e.g., AccountCoverage)
207            let field_name_pascal = crate::naming::Naming::to_pascal_case(field_name);
208            let nested_type_name = format!("{resource_name}{field_name_pascal}");
209
210            let rust_type = RustType::Custom(nested_type_name);
211
212            if is_array {
213                RustType::Vec(Box::new(rust_type))
214            } else {
215                rust_type
216            }
217        } else {
218            // Fallback to BackboneElement if we can't determine the specific type
219            let rust_type = RustType::Custom("BackboneElement".to_string());
220            if is_array {
221                RustType::Vec(Box::new(rust_type))
222            } else {
223                rust_type
224            }
225        }
226    }
227
228    fn add_choice_type_accessor_methods(
229        &self,
230        _rust_trait: &mut RustTrait,
231        _structure_def: &StructureDefinition,
232    ) -> CodegenResult<()> {
233        // Implementation for choice type accessors can be added here.
234        Ok(())
235    }
236}