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    #[derive(Clone)]
278    pub enum Max {
279        Unlimited,
280        Fixed(usize),
281    }
282
283    pub fn cardinality(element: &ElementDefinition) -> (usize, Max) {
284        let min = element.min.as_ref().and_then(|m| m.value).map_or(0, |m| m) as usize;
285
286        let max = element
287            .max
288            .as_ref()
289            .and_then(|m| m.value.as_ref())
290            .map(|v| v.as_str())
291            .and_then(|s| {
292                if s == "*" {
293                    Some(Max::Unlimited)
294                } else {
295                    s.parse::<usize>().ok().and_then(|i| Some(Max::Fixed(i)))
296                }
297            });
298
299        (min, max.unwrap_or_else(|| Max::Fixed(1)))
300    }
301}
302
303pub mod generate {
304    use std::collections::HashMap;
305
306    use haste_fhir_model::r4::generated::{
307        resources::StructureDefinition, types::ElementDefinition,
308    };
309    use proc_macro2::TokenStream;
310    use quote::{format_ident, quote};
311
312    use crate::utilities::{FHIR_PRIMITIVES, conditionals, conversion, extract};
313
314    /// Capitalize the first character in s.
315    pub fn capitalize(s: &str) -> String {
316        let mut c = s.chars();
317        match c.next() {
318            None => String::new(),
319            Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
320        }
321    }
322
323    pub fn struct_name(sd: &StructureDefinition, element: &ElementDefinition) -> String {
324        if conditionals::is_root(sd, element) {
325            let mut interface_name: String = capitalize(sd.id.as_ref().unwrap());
326            if conditionals::is_primitive_sd(sd) {
327                interface_name = "FHIR".to_owned() + &interface_name;
328            }
329            interface_name
330        } else {
331            element
332                .id
333                .as_ref()
334                .map(|p| p.split("."))
335                .map(|p| p.map(capitalize).collect::<Vec<String>>().join(""))
336                .unwrap()
337                .replace("[x]", "")
338        }
339    }
340
341    pub fn type_choice_name(sd: &StructureDefinition, element: &ElementDefinition) -> String {
342        let name = struct_name(sd, element);
343        name + "TypeChoice"
344    }
345
346    pub fn type_choice_variant_name(element: &ElementDefinition, fhir_type: &str) -> String {
347        let field_name = extract::field_name(&extract::path(element));
348        format!("{:0}{:1}", field_name, capitalize(fhir_type))
349    }
350
351    pub fn create_type_choice_variants(element: &ElementDefinition) -> Vec<String> {
352        extract::field_types(element)
353            .into_iter()
354            .map(|fhir_type| type_choice_variant_name(element, fhir_type))
355            .collect()
356    }
357    pub fn create_type_choice_primitive_variants(element: &ElementDefinition) -> Vec<String> {
358        extract::field_types(element)
359            .into_iter()
360            .filter(|fhir_type| FHIR_PRIMITIVES.contains_key(*fhir_type))
361            .map(|fhir_type| type_choice_variant_name(element, fhir_type))
362            .collect()
363    }
364
365    pub fn field_typename(
366        sd: &StructureDefinition,
367        element: &ElementDefinition,
368        inlined_terminology: &HashMap<String, String>,
369    ) -> TokenStream {
370        let field_value_type_name = if conditionals::is_typechoice(element) {
371            let k = format_ident!("{}", type_choice_name(sd, element));
372            quote! {
373                #k
374            }
375        } else if conditionals::is_nested_complex(element) {
376            let k = format_ident!("{}", struct_name(sd, element));
377            quote! {
378                #k
379            }
380        } else {
381            let fhir_type = element.type_.as_ref().unwrap()[0]
382                .code
383                .as_ref()
384                .value
385                .as_ref()
386                .unwrap();
387
388            conversion::fhir_type_to_rust_type(element, fhir_type, inlined_terminology)
389        };
390
391        field_value_type_name
392    }
393}
394
395pub mod conditionals {
396    use haste_fhir_model::r4::generated::{
397        resources::StructureDefinition, terminology::StructureDefinitionKind,
398        types::ElementDefinition,
399    };
400
401    use crate::utilities::{FHIR_PRIMITIVES, RUST_PRIMITIVES, extract};
402
403    pub fn is_root(sd: &StructureDefinition, element: &ElementDefinition) -> bool {
404        element.path.value == sd.id
405    }
406
407    pub fn is_resource_sd(sd: &StructureDefinition) -> bool {
408        if let StructureDefinitionKind::Resource(_) = sd.kind.as_ref() {
409            true
410        } else {
411            false
412        }
413    }
414
415    pub fn is_primitive(element: &ElementDefinition) -> bool {
416        let types = extract::field_types(element);
417        types.len() == 1 && FHIR_PRIMITIVES.contains_key(types[0])
418    }
419
420    pub fn is_nested_complex(element: &ElementDefinition) -> bool {
421        let types = extract::field_types(element);
422        // Backbone or Typechoice elements Have inlined types created.
423        types.len() > 1 || types[0] == "BackboneElement" || types[0] == "Element"
424    }
425
426    // All structs should be boxed if they are not rust primitive types.
427    pub fn should_be_boxed(fhir_type: &str) -> bool {
428        !RUST_PRIMITIVES.contains_key(fhir_type)
429    }
430
431    pub fn is_primitive_sd(sd: &StructureDefinition) -> bool {
432        if let StructureDefinitionKind::PrimitiveType(_) = sd.kind.as_ref() {
433            true
434        } else {
435            false
436        }
437    }
438
439    pub fn is_typechoice(element: &ElementDefinition) -> bool {
440        extract::field_types(element).len() > 1
441    }
442}
443
444pub mod load {
445    use std::path::Path;
446
447    use haste_fhir_model::r4::generated::{
448        resources::{Resource, StructureDefinition},
449        terminology::StructureDefinitionKind,
450    };
451
452    use crate::utilities::extract;
453
454    pub fn load_from_file(file_path: &Path) -> Result<Resource, String> {
455        let data = std::fs::read_to_string(file_path)
456            .map_err(|e| format!("Failed to read file: {}", e))?;
457
458        let resource = haste_fhir_serialization_json::from_str::<Resource>(&data)
459            .map_err(|e| format!("Failed to parse JSON: {}", e))?;
460
461        Ok(resource)
462    }
463
464    pub fn get_structure_definitions<'a>(
465        resource: &'a Resource,
466        level: Option<&'static str>,
467    ) -> Result<Vec<&'a StructureDefinition>, String> {
468        match resource {
469            Resource::Bundle(bundle) => {
470                if let Some(entries) = bundle.entry.as_ref() {
471                    let sds = entries
472                        .iter()
473                        .filter_map(|e| e.resource.as_ref())
474                        .filter_map(|sd| match sd.as_ref() {
475                            Resource::StructureDefinition(sd) => Some(sd),
476                            _ => None,
477                        });
478
479                    let filtered_sds = sds.filter(move |sd| {
480                        if let Some(level) = level {
481                            match sd.kind.as_ref() {
482                                StructureDefinitionKind::Resource(_)
483                                | StructureDefinitionKind::Null(_) => level == "resource",
484                                StructureDefinitionKind::ComplexType(_) => level == "complex-type",
485                                StructureDefinitionKind::PrimitiveType(_) => {
486                                    level == "primitive-type"
487                                }
488                                _ => false,
489                            }
490                        } else {
491                            true
492                        }
493                    });
494
495                    Ok(filtered_sds.collect())
496                } else {
497                    Ok(vec![])
498                }
499            }
500            Resource::StructureDefinition(sd) => {
501                let resources = std::iter::once(sd);
502                let filtered_resources = resources.filter(|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(_) => level == "primitive-type",
509                            _ => false,
510                        }
511                    } else {
512                        true
513                    }
514                });
515
516                Ok(filtered_resources.collect())
517            }
518            _ => Ok(vec![]),
519        }
520    }
521}