1use crate::config::CodegenConfig;
7use crate::fhir_types::ElementType;
8use crate::rust_types::RustType;
9use crate::value_sets::ValueSetManager;
10
11#[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 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 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()); }
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 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 fn generate_enum_for_required_binding(
65 &mut self,
66 url: &str,
67 version: Option<&str>,
68 ) -> Option<String> {
69 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 Some(self.value_set_manager.generate_placeholder_enum(url))
78 }
79 }
80 }
81
82 #[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 fn map_single_fhir_type_with_binding(
90 &mut self,
91 element_type: &ElementType,
92 binding: Option<&crate::fhir_types::ElementBinding>,
93 ) -> RustType {
94 let code = match &element_type.code {
96 Some(c) => c,
97 None => return RustType::Custom("StringType".to_string()),
98 };
99
100 if let Some(rust_type) = self.config.type_mappings.get(code) {
102 return self.parse_rust_type_string(rust_type);
103 }
104
105 match code.as_str() {
107 "string" => RustType::Custom("StringType".to_string()),
109 "markdown" => RustType::Custom("StringType".to_string()), "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" => 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 "base64Binary" => RustType::Custom("Base64BinaryType".to_string()),
130
131 "code" => {
133 if let Some(binding) = binding {
134 if binding.strength == "required" {
135 if let Some(value_set_url) = &binding.value_set {
136 let (url, version) = self.parse_valueset_url(value_set_url);
138
139 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 RustType::Custom("StringType".to_string())
150 }
151
152 "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" => RustType::Custom("Extension".to_string()),
174
175 "BackboneElement" => RustType::Custom("BackboneElement".to_string()),
177 "ElementDefinition" => RustType::Custom("ElementDefinition".to_string()),
178
179 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_type if self.is_resource_type(resource_type) => {
195 RustType::Custom(resource_type.to_string())
196 }
197
198 _ => {
200 eprintln!("Warning: Unknown FHIR type '{code}', defaulting to StringType");
201 RustType::Custom("StringType".to_string())
202 }
203 }
204 }
205
206 fn handle_reference_type(&mut self, _element_type: &ElementType) -> RustType {
208 RustType::Custom("Reference".to_string())
211 }
212
213 #[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 fn is_resource_type(&self, type_name: &str) -> bool {
225 type_name.chars().next().is_some_and(|c| c.is_uppercase())
228 && !matches!(type_name, "String" | "Boolean" | "Integer" | "Float")
229 }
230
231 #[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 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 = match self.value_set_manager.get_enum_name(value_set_url) {
255 Some(name) => name.clone(),
256 None => {
257 return RustType::Custom("StringType".to_string());
258 }
259 };
260 RustType::Custom(enum_name)
261 } else {
262 let enum_name = self
263 .value_set_manager
264 .generate_placeholder_enum(value_set_url);
265 RustType::Custom(enum_name)
266 }
267 }
268
269 pub fn is_optional(
271 &self,
272 min_cardinality: Option<u32>,
273 _max_cardinality: Option<&str>,
274 ) -> bool {
275 match min_cardinality {
276 Some(0) => true,
277 Some(_) => false,
278 None => true, }
280 }
281
282 pub fn is_array(&self, max_cardinality: Option<&str>) -> bool {
284 match max_cardinality {
285 Some("1") => false,
286 Some("0") => false,
287 Some(_) => true, None => false,
289 }
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use crate::config::CodegenConfig;
297
298 #[test]
299 fn test_primitive_type_mapping() {
300 let config = CodegenConfig::default();
301 let mut value_set_manager = ValueSetManager::new();
302 let mut mapper = TypeMapper::new(&config, &mut value_set_manager);
303
304 let string_type = ElementType {
305 code: Some("string".to_string()),
306 target_profile: None,
307 };
308
309 let result = mapper.map_single_fhir_type(&string_type);
310 assert!(matches!(
311 result,
312 RustType::Custom(ref name) if name == "StringType"
313 ));
314
315 let boolean_type = ElementType {
316 code: Some("boolean".to_string()),
317 target_profile: None,
318 };
319
320 assert!(matches!(
321 mapper.map_single_fhir_type(&boolean_type),
322 RustType::Custom(ref name) if name == "BooleanType"
323 ));
324 }
325
326 #[test]
327 fn test_cardinality_checks() {
328 let config = CodegenConfig::default();
329 let mut value_set_manager = ValueSetManager::new();
330 let mapper = TypeMapper::new(&config, &mut value_set_manager);
331
332 assert!(mapper.is_optional(Some(0), Some("1")));
333 assert!(!mapper.is_optional(Some(1), Some("1")));
334 assert!(mapper.is_optional(None, Some("1")));
335
336 assert!(!mapper.is_array(Some("1")));
337 assert!(mapper.is_array(Some("*")));
338 assert!(mapper.is_array(Some("5")));
339 }
340}