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 = 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 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, }
279 }
280
281 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, 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}