Skip to main content

haste_codegen/
utilities.rs

1#![allow(unused)]
2use std::{
3    collections::{HashMap, HashSet},
4    sync::LazyLock,
5};
6
7/// Some of these keywords are present as properties in the FHIR spec.
8/// We need to prefix them with an underscore to avoid conflicts.
9/// And use an attribute to rename the field in the generated code.
10pub static RUST_KEYWORDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
11    let mut m = HashSet::new();
12    m.insert("self");
13    m.insert("Self");
14    m.insert("super");
15    m.insert("type");
16    m.insert("use");
17    m.insert("identifier");
18    m.insert("abstract");
19    m.insert("for");
20    m.insert("if");
21    m.insert("else");
22    m.insert("match");
23    m.insert("while");
24    m.insert("loop");
25    m.insert("break");
26    m.insert("continue");
27    m.insert("ref");
28    m.insert("return");
29    m.insert("async");
30    m
31});
32
33pub static RUST_PRIMITIVES: LazyLock<HashMap<String, String>> = LazyLock::new(|| {
34    let mut m = HashMap::new();
35    m.insert(
36        "http://hl7.org/fhirpath/System.String".to_string(),
37        "String".to_string(),
38    );
39    m.insert(
40        "http://hl7.org/fhirpath/System.Decimal".to_string(),
41        "f64".to_string(),
42    );
43    m.insert(
44        "http://hl7.org/fhirpath/System.Boolean".to_string(),
45        "bool".to_string(),
46    );
47    m.insert(
48        "http://hl7.org/fhirpath/System.Integer".to_string(),
49        "i64".to_string(),
50    );
51    m.insert(
52        "http://hl7.org/fhirpath/System.Time".to_string(),
53        "crate::r4::datetime::Time".to_string(),
54    );
55    m.insert(
56        "http://hl7.org/fhirpath/System.Date".to_string(),
57        "crate::r4::datetime::Date".to_string(),
58    );
59    m.insert(
60        "http://hl7.org/fhirpath/System.DateTime".to_string(),
61        "crate::r4::datetime::DateTime".to_string(),
62    );
63    m.insert(
64        "http://hl7.org/fhirpath/System.Instant".to_string(),
65        "crate::r4::datetime::Instant".to_string(),
66    );
67    m
68});
69
70pub static FHIR_PRIMITIVES: LazyLock<HashMap<String, String>> = LazyLock::new(|| {
71    let mut m = HashMap::new();
72    // bool type
73    m.insert("boolean".to_string(), "FHIRBoolean".to_string());
74
75    // f64 type
76    m.insert("decimal".to_string(), "FHIRDecimal".to_string());
77
78    // i64 type
79    m.insert("integer".to_string(), "FHIRInteger".to_string());
80    // u64 type
81    m.insert("positiveInt".to_string(), "FHIRPositiveInt".to_string());
82    m.insert("unsignedInt".to_string(), "FHIRUnsignedInt".to_string());
83
84    // String type
85    m.insert("base64Binary".to_string(), "FHIRBase64Binary".to_string());
86    m.insert("canonical".to_string(), "FHIRCanonical".to_string());
87    m.insert("code".to_string(), "FHIRCode".to_string());
88    m.insert("id".to_string(), "FHIRId".to_string());
89    m.insert("markdown".to_string(), "FHIRMarkdown".to_string());
90    m.insert("oid".to_string(), "FHIROid".to_string());
91    m.insert("string".to_string(), "FHIRString".to_string());
92    m.insert("uri".to_string(), "FHIRUri".to_string());
93    m.insert("url".to_string(), "FHIRUrl".to_string());
94    m.insert("uuid".to_string(), "FHIRUuid".to_string());
95    m.insert("xhtml".to_string(), "FHIRXhtml".to_string());
96
97    // Date and Time types
98    m.insert("instant".to_string(), "FHIRInstant".to_string());
99    m.insert("date".to_string(), "FHIRDate".to_string());
100    m.insert("dateTime".to_string(), "FHIRDateTime".to_string());
101    m.insert("time".to_string(), "FHIRTime".to_string());
102
103    m
104});
105
106pub static FHIR_PRIMITIVE_VALUE_TYPE: LazyLock<HashMap<String, String>> = LazyLock::new(|| {
107    let mut m = HashMap::new();
108    // bool type
109    m.insert("boolean".to_string(), "bool".to_string());
110
111    // f64 type
112    m.insert("decimal".to_string(), "f64".to_string());
113
114    // i64 type
115    m.insert("integer".to_string(), "i64".to_string());
116    // u64 type
117    m.insert("positiveInt".to_string(), "u64".to_string());
118    m.insert("unsignedInt".to_string(), "u64".to_string());
119
120    // String type
121    m.insert("base64Binary".to_string(), "String".to_string());
122    m.insert("canonical".to_string(), "String".to_string());
123    m.insert("code".to_string(), "String".to_string());
124    m.insert("date".to_string(), "String".to_string());
125    m.insert("dateTime".to_string(), "String".to_string());
126    m.insert("id".to_string(), "String".to_string());
127    m.insert("instant".to_string(), "String".to_string());
128    m.insert("markdown".to_string(), "String".to_string());
129    m.insert("oid".to_string(), "String".to_string());
130    m.insert("string".to_string(), "String".to_string());
131    m.insert("time".to_string(), "String".to_string());
132    m.insert("uri".to_string(), "String".to_string());
133    m.insert("url".to_string(), "String".to_string());
134    m.insert("uuid".to_string(), "String".to_string());
135    m.insert("xhtml".to_string(), "String".to_string());
136
137    m
138});
139
140pub mod conversion {
141    use std::collections::HashMap;
142
143    use super::{FHIR_PRIMITIVES, RUST_PRIMITIVES};
144    use haste_fhir_model::r4::generated::{terminology::BindingStrength, types::ElementDefinition};
145    use proc_macro2::TokenStream;
146    use quote::{format_ident, quote};
147
148    pub fn fhir_type_to_rust_type(
149        element: &ElementDefinition,
150        fhir_type: &str,
151        inlined_terminology: &HashMap<String, String>,
152    ) -> TokenStream {
153        let path = element.path.value.as_ref().map(|p| p.as_str());
154
155        match path {
156            Some("unsignedInt.value") | Some("positiveInt.value") => {
157                let k = format_ident!("{}", "u64");
158                quote! {
159                    #k
160                }
161            }
162
163            _ => {
164                if let Some(rust_primitive) = RUST_PRIMITIVES.get(fhir_type) {
165                    // Special handling for instance which should use instant type,
166                    let path = path.unwrap();
167                    if path == "instant.value" {
168                        let k = RUST_PRIMITIVES
169                            .get("http://hl7.org/fhirpath/System.Instant")
170                            .unwrap()
171                            .parse::<TokenStream>()
172                            .unwrap();
173
174                        quote! {
175                            #k
176                        }
177                    } else {
178                        let k = rust_primitive.parse::<TokenStream>().unwrap();
179                        quote! {
180                            #k
181                        }
182                    }
183                } else if let Some(primitive) = FHIR_PRIMITIVES.get(fhir_type) {
184                    // Support for inlined types.
185                    // inlined could be a url | version for canonical.
186                    // Only do inlined if the binding is required and exists as inlined terminology.
187
188                    if let Some(BindingStrength::Required(_)) =
189                        element.binding.as_ref().map(|b| b.strength.as_ref())
190                        && let Some(canonical_string) = element
191                            .binding
192                            .as_ref()
193                            .and_then(|b| b.valueSet.as_ref())
194                            .and_then(|b| b.value.as_ref())
195                            .map(|u| u.as_str())
196                        && let Some(url) = canonical_string.split('|').next()
197                        && let Some(inlined) = inlined_terminology.get(url)
198                    {
199                        let inline_type = format_ident!("{}", inlined);
200                        quote! {
201                            Box<terminology::#inline_type>
202                        }
203                    } else {
204                        let k = format_ident!("{}", primitive.clone());
205                        quote! {
206                            Box<#k>
207                        }
208                    }
209                } else {
210                    let k = format_ident!("{}", fhir_type.to_string());
211                    quote! {
212                        Box<#k>
213                    }
214                }
215            }
216        }
217    }
218}
219
220pub mod extract {
221    use haste_fhir_model::r4::generated::resources::StructureDefinition;
222    use haste_fhir_model::r4::generated::types::ElementDefinition;
223    pub fn field_types<'a>(element: &ElementDefinition) -> Vec<&str> {
224        let codes = element
225            .type_
226            .as_ref()
227            .map(|types| {
228                types
229                    .iter()
230                    .filter_map(|t| t.code.value.as_ref().map(|v| v.as_str()))
231                    .collect()
232            })
233            .unwrap_or_else(|| vec![]);
234        codes
235    }
236
237    pub fn field_name(path: &str) -> String {
238        let field_name: String = path
239            .split('.')
240            .last()
241            .unwrap_or("")
242            .chars()
243            .enumerate()
244            .map(|(i, c)| {
245                if i == 0 {
246                    c.to_lowercase().next().unwrap_or(c)
247                } else {
248                    c
249                }
250            })
251            .collect();
252        let removed_x = if field_name.ends_with("[x]") {
253            field_name.replace("[x]", "")
254        } else {
255            field_name.clone()
256        };
257
258        removed_x
259    }
260
261    pub fn is_abstract(sd: &StructureDefinition) -> bool {
262        sd.abstract_.value == Some(true)
263    }
264
265    pub fn path(element: &ElementDefinition) -> String {
266        element.path.value.clone().unwrap_or_else(|| "".to_string())
267    }
268    pub fn element_description(element: &ElementDefinition) -> String {
269        element
270            .definition
271            .as_ref()
272            .and_then(|d| d.value.as_ref())
273            .cloned()
274            .unwrap_or_else(|| "".to_string())
275    }
276
277    pub fn fhir_type(sd: &StructureDefinition, element: &ElementDefinition) -> String {
278        if crate::utilities::conditionals::is_root(sd, element) {
279            sd.type_
280                .value
281                .as_ref()
282                .expect("Root element must have a type")
283                .clone()
284        } else {
285            let default_types = vec![];
286            let fhir_types = element.type_.as_ref().unwrap_or(&default_types);
287            if fhir_types.len() == 1 {
288                fhir_types[0]
289                    .code
290                    .value
291                    .as_ref()
292                    .expect("Type must have a code")
293                    .clone()
294            } else {
295                panic!("Element has multiple types, cannot determine FHIR type");
296            }
297        }
298    }
299
300    #[derive(Clone)]
301    pub enum Max {
302        Unlimited,
303        Fixed(usize),
304    }
305
306    pub fn cardinality(element: &ElementDefinition) -> (usize, Max) {
307        let min = element.min.as_ref().and_then(|m| m.value).map_or(0, |m| m) as usize;
308
309        let max = element
310            .max
311            .as_ref()
312            .and_then(|m| m.value.as_ref())
313            .map(|v| v.as_str())
314            .and_then(|s| {
315                if s == "*" {
316                    Some(Max::Unlimited)
317                } else {
318                    s.parse::<usize>().ok().and_then(|i| Some(Max::Fixed(i)))
319                }
320            });
321
322        (min, max.unwrap_or_else(|| Max::Fixed(1)))
323    }
324}
325
326pub mod generate {
327    use std::collections::HashMap;
328
329    use haste_fhir_model::r4::generated::{
330        resources::StructureDefinition, types::ElementDefinition,
331    };
332    use proc_macro2::TokenStream;
333    use quote::{format_ident, quote};
334
335    use crate::utilities::{FHIR_PRIMITIVES, conditionals, conversion, extract};
336
337    /// Capitalize the first character in s.
338    pub fn capitalize(s: &str) -> String {
339        let mut c = s.chars();
340        match c.next() {
341            None => String::new(),
342            Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
343        }
344    }
345
346    pub fn struct_name(sd: &StructureDefinition, element: &ElementDefinition) -> String {
347        if conditionals::is_root(sd, element) {
348            let mut interface_name: String = capitalize(sd.id.as_ref().unwrap());
349            if conditionals::is_primitive_sd(sd) {
350                interface_name = "FHIR".to_owned() + &interface_name;
351            }
352            interface_name
353        } else {
354            element
355                .id
356                .as_ref()
357                .map(|p| p.split("."))
358                .map(|p| p.map(capitalize).collect::<Vec<String>>().join(""))
359                .unwrap()
360                .replace("[x]", "")
361        }
362    }
363
364    pub fn type_choice_name(sd: &StructureDefinition, element: &ElementDefinition) -> String {
365        let name = struct_name(sd, element);
366        name + "TypeChoice"
367    }
368
369    pub fn type_choice_variant_name(element: &ElementDefinition, fhir_type: &str) -> String {
370        let field_name = extract::field_name(&extract::path(element));
371        format!("{:0}{:1}", field_name, capitalize(fhir_type))
372    }
373
374    pub fn create_type_choice_variants(element: &ElementDefinition) -> Vec<String> {
375        extract::field_types(element)
376            .into_iter()
377            .map(|fhir_type| type_choice_variant_name(element, fhir_type))
378            .collect()
379    }
380    pub fn create_type_choice_primitive_variants(element: &ElementDefinition) -> Vec<String> {
381        extract::field_types(element)
382            .into_iter()
383            .filter(|fhir_type| FHIR_PRIMITIVES.contains_key(*fhir_type))
384            .map(|fhir_type| type_choice_variant_name(element, fhir_type))
385            .collect()
386    }
387
388    pub fn field_typename(
389        sd: &StructureDefinition,
390        element: &ElementDefinition,
391        inlined_terminology: &HashMap<String, String>,
392    ) -> TokenStream {
393        let field_value_type_name = if conditionals::is_typechoice(element) {
394            let k = format_ident!("{}", type_choice_name(sd, element));
395            quote! {
396                #k
397            }
398        } else if conditionals::is_nested_complex(element) {
399            let k = format_ident!("{}", struct_name(sd, element));
400            quote! {
401                #k
402            }
403        } else {
404            let fhir_type = element.type_.as_ref().unwrap()[0]
405                .code
406                .as_ref()
407                .value
408                .as_ref()
409                .unwrap();
410
411            conversion::fhir_type_to_rust_type(element, fhir_type, inlined_terminology)
412        };
413
414        field_value_type_name
415    }
416}
417
418pub mod conditionals {
419    use haste_fhir_model::r4::generated::{
420        resources::StructureDefinition, terminology::StructureDefinitionKind,
421        types::ElementDefinition,
422    };
423
424    use crate::utilities::{FHIR_PRIMITIVES, RUST_PRIMITIVES, extract};
425
426    pub fn is_root(sd: &StructureDefinition, element: &ElementDefinition) -> bool {
427        element.path.value == sd.id
428    }
429
430    pub fn is_resource_sd(sd: &StructureDefinition) -> bool {
431        if let StructureDefinitionKind::Resource(_) = sd.kind.as_ref() {
432            true
433        } else {
434            false
435        }
436    }
437
438    pub fn is_primitive(element: &ElementDefinition) -> bool {
439        let types = extract::field_types(element);
440        types.len() == 1 && FHIR_PRIMITIVES.contains_key(types[0])
441    }
442
443    pub fn is_nested_complex(element: &ElementDefinition) -> bool {
444        let types = extract::field_types(element);
445        // Backbone or Typechoice elements Have inlined types created.
446        types.len() > 1 || types[0] == "BackboneElement" || types[0] == "Element"
447    }
448
449    // All structs should be boxed if they are not rust primitive types.
450    pub fn should_be_boxed(fhir_type: &str) -> bool {
451        !RUST_PRIMITIVES.contains_key(fhir_type)
452    }
453
454    pub fn is_primitive_sd(sd: &StructureDefinition) -> bool {
455        if let StructureDefinitionKind::PrimitiveType(_) = sd.kind.as_ref() {
456            true
457        } else {
458            false
459        }
460    }
461
462    pub fn is_typechoice(element: &ElementDefinition) -> bool {
463        extract::field_types(element).len() > 1
464    }
465}
466
467pub mod load {
468    use std::path::Path;
469
470    use haste_fhir_model::r4::generated::{
471        resources::{Resource, StructureDefinition},
472        terminology::StructureDefinitionKind,
473    };
474
475    use crate::utilities::extract;
476
477    pub fn load_from_file(file_path: &Path) -> Result<Resource, String> {
478        let data = std::fs::read_to_string(file_path)
479            .map_err(|e| format!("Failed to read file: {}", e))?;
480
481        let resource = haste_fhir_serialization_json::from_str::<Resource>(&data)
482            .map_err(|e| format!("Failed to parse JSON: {}", e))?;
483
484        Ok(resource)
485    }
486
487    pub fn get_structure_definitions<'a>(
488        resource: &'a Resource,
489        level: Option<&'static str>,
490    ) -> Result<Vec<&'a StructureDefinition>, String> {
491        match resource {
492            Resource::Bundle(bundle) => {
493                if let Some(entries) = bundle.entry.as_ref() {
494                    let sds = entries
495                        .iter()
496                        .filter_map(|e| e.resource.as_ref())
497                        .filter_map(|sd| match sd.as_ref() {
498                            Resource::StructureDefinition(sd) => Some(sd),
499                            _ => None,
500                        });
501
502                    let filtered_sds = sds.filter(move |sd| {
503                        if let Some(level) = level {
504                            match sd.kind.as_ref() {
505                                StructureDefinitionKind::Resource(_)
506                                | StructureDefinitionKind::Null(_) => level == "resource",
507                                StructureDefinitionKind::ComplexType(_) => level == "complex-type",
508                                StructureDefinitionKind::PrimitiveType(_) => {
509                                    level == "primitive-type"
510                                }
511                                _ => false,
512                            }
513                        } else {
514                            true
515                        }
516                    });
517
518                    Ok(filtered_sds.collect())
519                } else {
520                    Ok(vec![])
521                }
522            }
523            Resource::StructureDefinition(sd) => {
524                let resources = std::iter::once(sd);
525                let filtered_resources = resources.filter(|sd| {
526                    if let Some(level) = level {
527                        match sd.kind.as_ref() {
528                            StructureDefinitionKind::Resource(_)
529                            | StructureDefinitionKind::Null(_) => level == "resource",
530                            StructureDefinitionKind::ComplexType(_) => level == "complex-type",
531                            StructureDefinitionKind::PrimitiveType(_) => level == "primitive-type",
532                            _ => false,
533                        }
534                    } else {
535                        true
536                    }
537                });
538
539                Ok(filtered_resources.collect())
540            }
541            _ => Ok(vec![]),
542        }
543    }
544}