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.last().unwrap().to_string();
102        let rust_field_name = crate::naming::Naming::field_name(&field_name);
103
104        let _is_optional = element.min.unwrap_or(0) == 0;
105        let is_array = element.max.as_deref() == Some("*")
106            || element
107                .max
108                .as_deref()
109                .unwrap_or("1")
110                .parse::<i32>()
111                .unwrap_or(1)
112                > 1;
113
114        // Use binding-aware type mapping
115        let rust_type = self.get_field_rust_type(element, &field_name)?;
116
117        // Always add set_ method
118        self.add_set_method(
119            rust_trait,
120            &rust_field_name,
121            &field_name,
122            &rust_type,
123            is_array,
124        )?;
125
126        // Add add_ method for arrays
127        if is_array {
128            self.add_add_method(rust_trait, &rust_field_name, &field_name, &rust_type)?;
129        }
130
131        Ok(())
132    }
133
134    fn add_set_method(
135        &self,
136        rust_trait: &mut RustTrait,
137        rust_field_name: &str,
138        field_name: &str,
139        rust_type: &RustType,
140        is_array: bool,
141    ) -> CodegenResult<()> {
142        let method_name = format!("set_{rust_field_name}");
143
144        let parameter_type = if is_array {
145            // For arrays, set method takes a Vec
146            RustType::Vec(Box::new(rust_type.clone()))
147        } else {
148            rust_type.clone()
149        };
150
151        let method = RustTraitMethod::new(method_name)
152            .with_doc(format!(
153                "Sets the {field_name} field and returns self for chaining."
154            ))
155            .with_parameter("value".to_string(), parameter_type)
156            .with_return_type(RustType::Custom("Self".to_string()))
157            .with_body(format!("self.{field_name} = value; self"))
158            .with_self_param(Some("self".to_string())); // Take self by value for builder pattern
159
160        rust_trait.add_method(method);
161        Ok(())
162    }
163
164    fn add_add_method(
165        &self,
166        rust_trait: &mut RustTrait,
167        rust_field_name: &str,
168        field_name: &str,
169        rust_type: &RustType,
170    ) -> CodegenResult<()> {
171        let method_name = format!("add_{rust_field_name}");
172
173        let method = RustTraitMethod::new(method_name)
174            .with_doc(format!(
175                "Adds an item to the {field_name} field and returns self for chaining."
176            ))
177            .with_parameter("item".to_string(), rust_type.clone())
178            .with_return_type(RustType::Custom("Self".to_string()))
179            .with_body(format!("self.{field_name}.push(item); self"))
180            .with_self_param(Some("self".to_string())); // Take self by value for builder pattern
181
182        rust_trait.add_method(method);
183        Ok(())
184    }
185
186    fn add_choice_type_mutator_methods(
187        &self,
188        _rust_trait: &mut RustTrait,
189        _structure_def: &StructureDefinition,
190    ) -> CodegenResult<()> {
191        // Implementation for choice type mutators can be added here.
192        Ok(())
193    }
194
195    /// Add a constructor method that creates an instance with default/empty values
196    fn add_constructor_method(
197        &self,
198        rust_trait: &mut RustTrait,
199        structure_def: &StructureDefinition,
200    ) -> CodegenResult<()> {
201        let struct_name = crate::naming::Naming::struct_name(structure_def);
202
203        // Determine the module path for the import
204        let is_profile = crate::generators::type_registry::TypeRegistry::is_profile(structure_def);
205        let module = if is_profile { "profiles" } else { "resources" };
206        let snake_name = crate::naming::Naming::to_snake_case(&struct_name);
207        let struct_import = format!(
208            "{crate_name}::{module}::{snake_name}::{struct_name}",
209            crate_name = &self.crate_name
210        );
211        let trait_import = format!(
212            "{crate_name}::traits::{snake_name}::{struct_name}Mutators",
213            crate_name = &self.crate_name
214        );
215
216        // Basic constructor with no parameters - supports method chaining
217        let new_method = RustTraitMethod::new("new".to_string())
218            .with_doc(format!(
219                "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```"
220            ))
221            .with_return_type(RustType::Custom("Self".to_string()))
222            .with_self_param(None); // No self parameter for constructor
223
224        rust_trait.add_method(new_method);
225
226        Ok(())
227    }
228
229    /// Get the Rust type for a field element, considering ValueSet bindings.
230    /// For code fields with required bindings, returns the enum type name.
231    /// Otherwise, delegates to TypeUtilities for standard type mapping.
232    fn get_field_rust_type(
233        &self,
234        element: &ElementDefinition,
235        field_name: &str,
236    ) -> CodegenResult<RustType> {
237        let Some(element_type) = element.element_type.as_ref().and_then(|t| t.first()) else {
238            return Ok(RustType::String);
239        };
240
241        let Some(code) = &element_type.code else {
242            return Ok(RustType::String);
243        };
244
245        // Check if this is a code type with a required binding - if so, use enum type
246        if code == "code" {
247            if let Some(binding) = &element.binding {
248                if binding.strength == "required" {
249                    if let Some(value_set_url) = &binding.value_set {
250                        // Extract enum name from ValueSet URL
251                        // E.g., http://hl7.org/fhir/ValueSet/account-status|4.0.1 -> AccountStatus
252                        if let Some(enum_name) =
253                            self.extract_enum_name_from_value_set(value_set_url)
254                        {
255                            return Ok(RustType::Custom(enum_name));
256                        }
257                    }
258                }
259            }
260        }
261
262        // Otherwise, use the standard type mapping
263        TypeUtilities::map_fhir_type_to_rust(element_type, field_name, &element.path)
264    }
265
266    /// Extract enum type name from a ValueSet URL
267    /// E.g., "http://hl7.org/fhir/ValueSet/account-status" -> "AccountStatus"
268    fn extract_enum_name_from_value_set(&self, url: &str) -> Option<String> {
269        // Remove version suffix if present (e.g., |4.0.1)
270        let url_without_version = url.split('|').next().unwrap_or(url);
271
272        // Extract the last part after the last /
273        let value_set_name = url_without_version.split('/').next_back()?;
274
275        // Use the same logic as ValueSetManager::generate_enum_name for consistency
276        // Split on hyphens and capitalize each part to get PascalCase
277        let name = value_set_name
278            .split(&['-', '.'][..])
279            .filter(|part| !part.is_empty())
280            .map(|part| {
281                let mut chars = part.chars();
282                match chars.next() {
283                    None => String::new(),
284                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
285                }
286            })
287            .collect::<String>();
288
289        // Ensure it's a valid Rust identifier
290        if name.chars().next().unwrap_or('0').is_ascii_digit() {
291            Some(format!("ValueSet{name}"))
292        } else {
293            Some(name)
294        }
295    }
296}