Skip to main content

rh_codegen/
type_mapper.rs

1//! Type mapping utilities for FHIR to Rust type conversion
2//!
3//! This module handles the conversion of FHIR data types to appropriate Rust types,
4//! including handling of complex types, references, and custom mappings.
5
6use crate::config::CodegenConfig;
7use crate::fhir_types::ElementType;
8use crate::rust_types::RustType;
9use crate::value_sets::ValueSetManager;
10
11/// Handles mapping of FHIR types to Rust types
12#[derive(Debug)]
13pub struct TypeMapper<'a> {
14    config: &'a CodegenConfig,
15    value_set_manager: &'a mut ValueSetManager,
16}
17
18impl<'a> TypeMapper<'a> {
19    pub fn new(config: &'a CodegenConfig, value_set_manager: &'a mut ValueSetManager) -> Self {
20        Self {
21            config,
22            value_set_manager,
23        }
24    }
25
26    /// Map a FHIR type to a Rust type
27    pub fn map_fhir_type(&mut self, fhir_types: &[ElementType], is_array: bool) -> RustType {
28        self.map_fhir_type_with_binding(fhir_types, None, is_array)
29    }
30
31    /// Map a FHIR type to a Rust type, considering binding information for enum generation
32    pub fn map_fhir_type_with_binding(
33        &mut self,
34        fhir_types: &[ElementType],
35        binding: Option<&crate::fhir_types::ElementBinding>,
36        is_array: bool,
37    ) -> RustType {
38        if fhir_types.is_empty() {
39            return RustType::Custom("StringType".to_string()); // Default fallback to StringType
40        }
41
42        let primary_type = &fhir_types[0];
43        let rust_type = self.map_single_fhir_type_with_binding(primary_type, binding);
44
45        if is_array {
46            RustType::Vec(Box::new(rust_type))
47        } else {
48            rust_type
49        }
50    }
51
52    /// Parse ValueSet URL to extract URL and version
53    fn parse_valueset_url(&self, url: &str) -> (String, Option<String>) {
54        if let Some(pipe_pos) = url.find('|') {
55            let base_url = url[..pipe_pos].to_string();
56            let version = url[pipe_pos + 1..].to_string();
57            (base_url, Some(version))
58        } else {
59            (url.to_string(), None)
60        }
61    }
62
63    /// Generate enum for required ValueSet binding
64    fn generate_enum_for_required_binding(
65        &mut self,
66        url: &str,
67        version: Option<&str>,
68    ) -> Option<String> {
69        // Try to generate enum from ValueSet file
70        match self
71            .value_set_manager
72            .generate_enum_from_value_set(url, version)
73        {
74            Ok(enum_name) => Some(enum_name),
75            Err(_) => {
76                // Fallback to placeholder enum
77                Some(self.value_set_manager.generate_placeholder_enum(url))
78            }
79        }
80    }
81
82    /// Map a single FHIR ElementType to a Rust type
83    #[allow(dead_code)]
84    fn map_single_fhir_type(&mut self, element_type: &ElementType) -> RustType {
85        self.map_single_fhir_type_with_binding(element_type, None)
86    }
87
88    /// Map a single FHIR ElementType to a Rust type, considering binding information
89    fn map_single_fhir_type_with_binding(
90        &mut self,
91        element_type: &ElementType,
92        binding: Option<&crate::fhir_types::ElementBinding>,
93    ) -> RustType {
94        // Handle cases where code is missing - default to StringType
95        let code = match &element_type.code {
96            Some(c) => c,
97            None => return RustType::Custom("StringType".to_string()),
98        };
99
100        // Check for custom type mappings first
101        if let Some(rust_type) = self.config.type_mappings.get(code) {
102            return self.parse_rust_type_string(rust_type);
103        }
104
105        // Handle built-in FHIR types
106        match code.as_str() {
107            // Primitive types - use new primitive type aliases
108            "string" => RustType::Custom("StringType".to_string()),
109            "markdown" => RustType::Custom("StringType".to_string()), // markdown is string-based
110            "uri" => RustType::Custom("StringType".to_string()),
111            "url" => RustType::Custom("StringType".to_string()),
112            "canonical" => RustType::Custom("StringType".to_string()),
113            "oid" => RustType::Custom("StringType".to_string()),
114            "uuid" => RustType::Custom("StringType".to_string()),
115            "id" => RustType::Custom("StringType".to_string()),
116            "integer" => RustType::Custom("IntegerType".to_string()),
117            "positiveInt" => RustType::Custom("PositiveIntType".to_string()),
118            "unsignedInt" => RustType::Custom("UnsignedIntType".to_string()),
119            "boolean" => RustType::Custom("BooleanType".to_string()),
120            "decimal" => RustType::Custom("DecimalType".to_string()),
121
122            // Date/time types
123            "date" => RustType::Custom("DateType".to_string()),
124            "dateTime" => RustType::Custom("DateTimeType".to_string()),
125            "instant" => RustType::Custom("InstantType".to_string()),
126            "time" => RustType::Custom("TimeType".to_string()),
127
128            // Binary data
129            "base64Binary" => RustType::Custom("Base64BinaryType".to_string()),
130
131            // Code types - check for required binding and generate enum
132            "code" => {
133                if let Some(binding) = binding {
134                    if binding.strength == "required" {
135                        if let Some(value_set_url) = &binding.value_set {
136                            // Parse ValueSet URL and version
137                            let (url, version) = self.parse_valueset_url(value_set_url);
138
139                            // Generate enum for required binding
140                            if let Some(enum_name) =
141                                self.generate_enum_for_required_binding(&url, version.as_deref())
142                            {
143                                return RustType::Custom(enum_name);
144                            }
145                        }
146                    }
147                }
148                // Fall back to StringType for non-required bindings or when enum generation fails
149                RustType::Custom("StringType".to_string())
150            }
151
152            // Complex types
153            "Reference" => self.handle_reference_type(element_type),
154            "CodeableConcept" => RustType::Custom("CodeableConcept".to_string()),
155            "Coding" => RustType::Custom("Coding".to_string()),
156            "Identifier" => RustType::Custom("Identifier".to_string()),
157            "Period" => RustType::Custom("Period".to_string()),
158            "Quantity" => RustType::Custom("Quantity".to_string()),
159            "Range" => RustType::Custom("Range".to_string()),
160            "Ratio" => RustType::Custom("Ratio".to_string()),
161            "SampledData" => RustType::Custom("SampledData".to_string()),
162            "Attachment" => RustType::Custom("Attachment".to_string()),
163            "ContactPoint" => RustType::Custom("ContactPoint".to_string()),
164            "HumanName" => RustType::Custom("HumanName".to_string()),
165            "Address" => RustType::Custom("Address".to_string()),
166            "Age" => RustType::Custom("Age".to_string()),
167            "Count" => RustType::Custom("Count".to_string()),
168            "Distance" => RustType::Custom("Distance".to_string()),
169            "Duration" => RustType::Custom("Duration".to_string()),
170            "Money" => RustType::Custom("Money".to_string()),
171
172            // Extension type
173            "Extension" => RustType::Custom("Extension".to_string()),
174
175            // StructureDefinition sub-types
176            "BackboneElement" => RustType::Custom("BackboneElement".to_string()),
177            "ElementDefinition" => RustType::Custom("ElementDefinition".to_string()),
178
179            // Handle FHIRPath system types
180            typ if typ.starts_with("http://hl7.org/fhirpath/System.") => {
181                let system_type = typ
182                    .strip_prefix("http://hl7.org/fhirpath/System.")
183                    .unwrap_or("String");
184                match system_type {
185                    "String" => RustType::Custom("StringType".to_string()),
186                    "Integer" => RustType::Custom("IntegerType".to_string()),
187                    "Boolean" => RustType::Custom("BooleanType".to_string()),
188                    "Decimal" => RustType::Custom("DecimalType".to_string()),
189                    _ => RustType::Custom("StringType".to_string()),
190                }
191            }
192
193            // Resource types - use the type name directly
194            resource_type if self.is_resource_type(resource_type) => {
195                RustType::Custom(resource_type.to_string())
196            }
197
198            // Unknown type - default to StringType
199            _ => {
200                eprintln!("Warning: Unknown FHIR type '{code}', defaulting to StringType");
201                RustType::Custom("StringType".to_string())
202            }
203        }
204    }
205
206    /// Handle Reference types with target profiles
207    fn handle_reference_type(&mut self, _element_type: &ElementType) -> RustType {
208        // For now, just use a generic Reference type
209        // In the future, we could generate different Reference types for different targets
210        RustType::Custom("Reference".to_string())
211    }
212
213    /// Extract resource name from a profile URL
214    #[allow(dead_code)]
215    fn extract_resource_name(&self, profile_url: &str) -> String {
216        profile_url
217            .split('/')
218            .next_back()
219            .unwrap_or("Resource")
220            .to_string()
221    }
222
223    /// Check if a type name represents a FHIR resource
224    fn is_resource_type(&self, type_name: &str) -> bool {
225        // This is a simplified check - in a real implementation, you might want
226        // to maintain a comprehensive list of FHIR resource types
227        type_name.chars().next().is_some_and(|c| c.is_uppercase())
228            && !matches!(type_name, "String" | "Boolean" | "Integer" | "Float")
229    }
230
231    /// Parse a Rust type string from configuration
232    #[allow(clippy::only_used_in_recursion)]
233    fn parse_rust_type_string(&self, type_str: &str) -> RustType {
234        match type_str {
235            "String" => RustType::String,
236            "i32" => RustType::Integer,
237            "bool" => RustType::Boolean,
238            "f64" => RustType::Float,
239            s if s.starts_with("Option<") && s.ends_with('>') => {
240                let inner = &s[7..s.len() - 1];
241                RustType::Option(Box::new(self.parse_rust_type_string(inner)))
242            }
243            s if s.starts_with("Vec<") && s.ends_with('>') => {
244                let inner = &s[4..s.len() - 1];
245                RustType::Vec(Box::new(self.parse_rust_type_string(inner)))
246            }
247            _ => RustType::Custom(type_str.to_string()),
248        }
249    }
250
251    /// Get the appropriate Rust type for a ValueSet binding
252    pub fn get_value_set_type(&mut self, value_set_url: &str) -> RustType {
253        if self.value_set_manager.is_cached(value_set_url) {
254            let enum_name = self
255                .value_set_manager
256                .get_enum_name(value_set_url)
257                .expect("Cached ValueSet should have enum name")
258                .clone();
259            RustType::Custom(enum_name)
260        } else {
261            let enum_name = self
262                .value_set_manager
263                .generate_placeholder_enum(value_set_url);
264            RustType::Custom(enum_name)
265        }
266    }
267
268    /// Determine if a field should be optional based on FHIR cardinality
269    pub fn is_optional(
270        &self,
271        min_cardinality: Option<u32>,
272        _max_cardinality: Option<&str>,
273    ) -> bool {
274        match min_cardinality {
275            Some(0) => true,
276            Some(_) => false,
277            None => true, // Default to optional if not specified
278        }
279    }
280
281    /// Determine if a field represents an array based on FHIR cardinality
282    pub fn is_array(&self, max_cardinality: Option<&str>) -> bool {
283        match max_cardinality {
284            Some("1") => false,
285            Some("0") => false,
286            Some(_) => true, // "*", numbers > 1
287            None => false,
288        }
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use crate::config::CodegenConfig;
296
297    #[test]
298    fn test_primitive_type_mapping() {
299        let config = CodegenConfig::default();
300        let mut value_set_manager = ValueSetManager::new();
301        let mut mapper = TypeMapper::new(&config, &mut value_set_manager);
302
303        let string_type = ElementType {
304            code: Some("string".to_string()),
305            target_profile: None,
306        };
307
308        let result = mapper.map_single_fhir_type(&string_type);
309        assert!(matches!(
310            result,
311            RustType::Custom(ref name) if name == "StringType"
312        ));
313
314        let boolean_type = ElementType {
315            code: Some("boolean".to_string()),
316            target_profile: None,
317        };
318
319        assert!(matches!(
320            mapper.map_single_fhir_type(&boolean_type),
321            RustType::Custom(ref name) if name == "BooleanType"
322        ));
323    }
324
325    #[test]
326    fn test_cardinality_checks() {
327        let config = CodegenConfig::default();
328        let mut value_set_manager = ValueSetManager::new();
329        let mapper = TypeMapper::new(&config, &mut value_set_manager);
330
331        assert!(mapper.is_optional(Some(0), Some("1")));
332        assert!(!mapper.is_optional(Some(1), Some("1")));
333        assert!(mapper.is_optional(None, Some("1")));
334
335        assert!(!mapper.is_array(Some("1")));
336        assert!(mapper.is_array(Some("*")));
337        assert!(mapper.is_array(Some("5")));
338    }
339}