Skip to main content

rh_codegen/generators/
mutator_trait_generator.rs

1//! Generator for mutator traits.
2use crate::fhir_types::{ElementDefinition, StructureDefinition};
3use crate::generators::TypeUtilities;
4use crate::rust_types::{RustTrait, RustTraitMethod, RustType};
5use crate::CodegenResult;
6
7pub struct MutatorTraitGenerator {
8    crate_name: String,
9}
10
11impl Default for MutatorTraitGenerator {
12    fn default() -> Self {
13        Self {
14            crate_name: "hl7_fhir_r4_core".to_string(),
15        }
16    }
17}
18
19impl MutatorTraitGenerator {
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    pub fn with_crate_name(crate_name: impl Into<String>) -> Self {
25        Self {
26            crate_name: crate_name.into(),
27        }
28    }
29
30    pub fn add_mutator_methods(
31        &self,
32        rust_trait: &mut RustTrait,
33        structure_def: &StructureDefinition,
34    ) -> CodegenResult<()> {
35        // Add constructor method
36        self.add_constructor_method(rust_trait, structure_def)?;
37
38        let elements = structure_def
39            .differential
40            .as_ref()
41            .map_or(Vec::new(), |d| d.element.clone());
42
43        if elements.is_empty() {
44            if let Some(snapshot) = &structure_def.snapshot {
45                let snapshot_elements = snapshot.element.clone();
46                for element in &snapshot_elements {
47                    if self.should_generate_mutator(element, structure_def) {
48                        self.add_mutator_methods_for_element(rust_trait, element)?;
49                    }
50                }
51            }
52        } else {
53            for element in &elements {
54                if self.should_generate_mutator(element, structure_def) {
55                    self.add_mutator_methods_for_element(rust_trait, element)?;
56                }
57            }
58        }
59
60        self.add_choice_type_mutator_methods(rust_trait, structure_def)?;
61
62        Ok(())
63    }
64
65    fn should_generate_mutator(
66        &self,
67        element: &ElementDefinition,
68        structure_def: &StructureDefinition,
69    ) -> bool {
70        let field_path = &element.path;
71        let base_name = &structure_def.name;
72
73        // The path must start with the base name of the structure.
74        if !field_path.starts_with(base_name) {
75            return false;
76        }
77
78        // We are interested in direct fields of the resource, which have paths like "Patient.active".
79        // Splitting by '.' should result in exactly two parts.
80        let path_parts: Vec<&str> = field_path.split('.').collect();
81        if path_parts.len() != 2 {
82            return false;
83        }
84
85        // The first part must match the base name.
86        if path_parts[0] != base_name {
87            return false;
88        }
89
90        // We don't generate mutators for choice types here, they are handled separately.
91        let field_name = path_parts[1];
92        !field_name.ends_with("[x]")
93    }
94
95    fn add_mutator_methods_for_element(
96        &self,
97        rust_trait: &mut RustTrait,
98        element: &ElementDefinition,
99    ) -> CodegenResult<()> {
100        let path_parts: Vec<&str> = element.path.split('.').collect();
101        let field_name = path_parts
102            .last()
103            .ok_or_else(|| crate::CodegenError::MissingField {
104                field: format!("element path has no field segment: {}", element.path),
105            })?
106            .to_string();
107        let rust_field_name = crate::naming::Naming::field_name(&field_name);
108
109        let _is_optional = element.min.unwrap_or(0) == 0;
110        let is_array = element.max.as_deref() == Some("*")
111            || element
112                .max
113                .as_deref()
114                .unwrap_or("1")
115                .parse::<i32>()
116                .unwrap_or(1)
117                > 1;
118
119        // Use binding-aware type mapping
120        let rust_type = self.get_field_rust_type(element, &field_name)?;
121
122        // Always add set_ method
123        self.add_set_method(
124            rust_trait,
125            &rust_field_name,
126            &field_name,
127            &rust_type,
128            is_array,
129        )?;
130
131        // Add add_ method for arrays
132        if is_array {
133            self.add_add_method(rust_trait, &rust_field_name, &field_name, &rust_type)?;
134        }
135
136        Ok(())
137    }
138
139    fn add_set_method(
140        &self,
141        rust_trait: &mut RustTrait,
142        rust_field_name: &str,
143        field_name: &str,
144        rust_type: &RustType,
145        is_array: bool,
146    ) -> CodegenResult<()> {
147        let method_name = format!("set_{rust_field_name}");
148
149        let parameter_type = if is_array {
150            // For arrays, set method takes a Vec
151            RustType::Vec(Box::new(rust_type.clone()))
152        } else {
153            rust_type.clone()
154        };
155
156        let method = RustTraitMethod::new(method_name)
157            .with_doc(format!(
158                "Sets the {field_name} field and returns self for chaining."
159            ))
160            .with_parameter("value".to_string(), parameter_type)
161            .with_return_type(RustType::Custom("Self".to_string()))
162            .with_body(format!("self.{field_name} = value; self"))
163            .with_self_param(Some("self".to_string())); // Take self by value for builder pattern
164
165        rust_trait.add_method(method);
166        Ok(())
167    }
168
169    fn add_add_method(
170        &self,
171        rust_trait: &mut RustTrait,
172        rust_field_name: &str,
173        field_name: &str,
174        rust_type: &RustType,
175    ) -> CodegenResult<()> {
176        let method_name = format!("add_{rust_field_name}");
177
178        let method = RustTraitMethod::new(method_name)
179            .with_doc(format!(
180                "Adds an item to the {field_name} field and returns self for chaining."
181            ))
182            .with_parameter("item".to_string(), rust_type.clone())
183            .with_return_type(RustType::Custom("Self".to_string()))
184            .with_body(format!("self.{field_name}.push(item); self"))
185            .with_self_param(Some("self".to_string())); // Take self by value for builder pattern
186
187        rust_trait.add_method(method);
188        Ok(())
189    }
190
191    fn add_choice_type_mutator_methods(
192        &self,
193        _rust_trait: &mut RustTrait,
194        _structure_def: &StructureDefinition,
195    ) -> CodegenResult<()> {
196        // Implementation for choice type mutators can be added here.
197        Ok(())
198    }
199
200    /// Add a constructor method that creates an instance with default/empty values
201    fn add_constructor_method(
202        &self,
203        rust_trait: &mut RustTrait,
204        structure_def: &StructureDefinition,
205    ) -> CodegenResult<()> {
206        let struct_name = crate::naming::Naming::struct_name(structure_def);
207
208        // Determine the module path for the import
209        let is_profile = crate::generators::type_registry::TypeRegistry::is_profile(structure_def);
210        let module = if is_profile { "profiles" } else { "resources" };
211        let snake_name = crate::naming::Naming::to_snake_case(&struct_name);
212        let struct_import = format!(
213            "{crate_name}::{module}::{snake_name}::{struct_name}",
214            crate_name = &self.crate_name
215        );
216        let trait_import = format!(
217            "{crate_name}::traits::{snake_name}::{struct_name}Mutators",
218            crate_name = &self.crate_name
219        );
220
221        // Basic constructor with no parameters - supports method chaining
222        let new_method = RustTraitMethod::new("new".to_string())
223            .with_doc(format!(
224                "Create a new {struct_name} with default/empty values.\n\nAll optional fields will be set to None and array fields will be empty vectors.\nSupports method chaining with set_xxx() and add_xxx() methods.\n\n# Example\n```rust\nuse {struct_import};\nuse {trait_import};\n\nlet resource = {struct_name}::new();\n// Can be used with method chaining:\n// resource.set_field(value).add_item(item);\n```"
225            ))
226            .with_return_type(RustType::Custom("Self".to_string()))
227            .with_self_param(None); // No self parameter for constructor
228
229        rust_trait.add_method(new_method);
230
231        Ok(())
232    }
233
234    /// Get the Rust type for a field element, considering ValueSet bindings.
235    /// For code fields with required bindings, returns the enum type name.
236    /// Otherwise, delegates to TypeUtilities for standard type mapping.
237    fn get_field_rust_type(
238        &self,
239        element: &ElementDefinition,
240        field_name: &str,
241    ) -> CodegenResult<RustType> {
242        let Some(element_type) = element.element_type.as_ref().and_then(|t| t.first()) else {
243            return Ok(RustType::String);
244        };
245
246        let Some(code) = &element_type.code else {
247            return Ok(RustType::String);
248        };
249
250        // Check if this is a code type with a required binding - if so, use enum type
251        if code == "code" {
252            // `language` is always a plain string regardless of binding (BCP-47 code)
253            if field_name == "language" && element.path.ends_with(".language") {
254                return TypeUtilities::map_fhir_type_to_rust(
255                    element_type,
256                    field_name,
257                    &element.path,
258                );
259            }
260            if let Some(binding) = &element.binding {
261                if binding.strength == "required" {
262                    if let Some(value_set_url) = &binding.value_set {
263                        // Extract enum name from ValueSet URL
264                        // E.g., http://hl7.org/fhir/ValueSet/account-status|4.0.1 -> AccountStatus
265                        if let Some(enum_name) =
266                            self.extract_enum_name_from_value_set(value_set_url)
267                        {
268                            // Guard: if the binding enum has the same name as the containing
269                            // resource, fall back to String to avoid a self-referential type.
270                            let resource_name = element.path.split('.').next().unwrap_or("");
271                            if enum_name == resource_name {
272                                return TypeUtilities::map_fhir_type_to_rust(
273                                    element_type,
274                                    field_name,
275                                    &element.path,
276                                );
277                            }
278                            return Ok(RustType::Custom(enum_name));
279                        }
280                    }
281                }
282            }
283        }
284
285        // Otherwise, use the standard type mapping
286        TypeUtilities::map_fhir_type_to_rust(element_type, field_name, &element.path)
287    }
288
289    /// Extract enum type name from a ValueSet URL
290    /// E.g., "http://hl7.org/fhir/ValueSet/account-status" -> "AccountStatus"
291    fn extract_enum_name_from_value_set(&self, url: &str) -> Option<String> {
292        // Remove version suffix if present (e.g., |4.0.1)
293        let url_without_version = url.split('|').next().unwrap_or(url);
294
295        // Extract the last part after the last /
296        let value_set_name = url_without_version.split('/').next_back()?;
297
298        // Use the same logic as ValueSetManager::generate_enum_name for consistency
299        // Split on hyphens and capitalize each part to get PascalCase
300        let name = value_set_name
301            .split(&['-', '.'][..])
302            .filter(|part| !part.is_empty())
303            .map(|part| {
304                let mut chars = part.chars();
305                match chars.next() {
306                    None => String::new(),
307                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
308                }
309            })
310            .collect::<String>();
311
312        // Ensure it's a valid Rust identifier
313        if name.chars().next().unwrap_or('0').is_ascii_digit() {
314            Some(format!("ValueSet{name}"))
315        } else {
316            Some(name)
317        }
318    }
319}