fhirbolt_shared/
path.rs

1//! Module to track paths in the FHIR data model.
2
3use std::mem;
4
5use crate::{
6    element_map::{self, ElementMap, ElementSet},
7    type_hints::{self, TypeHints},
8    FhirRelease, FhirReleases,
9};
10
11const RESOURCE_COMMON_PRIMITIVE_FIELDS: &[&str] = &["implicitRules", "language"];
12const COMMON_SEQUENCE_FIELDS: &[&str] = &["extension", "modifierExtension"];
13
14fn type_hints(fhir_release: FhirRelease) -> &'static TypeHints {
15    match fhir_release {
16        FhirReleases::R4 => &type_hints::r4::TYPE_HINTS,
17        FhirReleases::R4B => &type_hints::r4b::TYPE_HINTS,
18        FhirReleases::R5 => &type_hints::r5::TYPE_HINTS,
19        _ => panic!("invalid FHIR release"),
20    }
21}
22
23fn element_map(fhir_release: FhirRelease) -> &'static ElementMap {
24    match fhir_release {
25        FhirReleases::R4 => &element_map::r4::ELEMENT_MAP,
26        FhirReleases::R4B => &element_map::r4b::ELEMENT_MAP,
27        FhirReleases::R5 => &element_map::r5::ELEMENT_MAP,
28        _ => panic!("invalid FHIR release"),
29    }
30}
31
32trait FirstLetterUppercase {
33    fn is_first_letter_uppercase(&self) -> bool;
34}
35
36impl FirstLetterUppercase for &str {
37    fn is_first_letter_uppercase(&self) -> bool {
38        self.chars()
39            .next()
40            .map(|c| c.is_uppercase())
41            .unwrap_or(false)
42    }
43}
44
45/// ElementPath is aware of the FHIR data model and tracks its position in the tree.
46///
47/// It can be used to query type information of its current path.
48#[derive(Debug, Clone)]
49pub struct ElementPath {
50    fhir_release: FhirRelease,
51    type_stack: TypeStack,
52}
53
54impl ElementPath {
55    #[inline]
56    pub fn new(fhir_release: FhirRelease) -> ElementPath {
57        ElementPath {
58            fhir_release,
59            type_stack: TypeStack::new(fhir_release),
60        }
61    }
62
63    #[inline]
64    pub fn is_empty(&self) -> bool {
65        self.type_stack.is_empty()
66    }
67
68    #[inline]
69    pub fn current_element(&self) -> Option<&str> {
70        self.current_type_path().last_split()
71    }
72
73    #[inline]
74    pub fn current_element_is_resource(&self) -> bool {
75        self.resolve_current_type() == Some("Resource")
76    }
77
78    #[inline]
79    pub fn current_element_is_extension(&self) -> bool {
80        matches!(
81            self.current_type_path().last_split(),
82            Some("extension") | Some("modifierExtension")
83        )
84    }
85
86    #[inline]
87    pub fn current_element_is_primitive(&self) -> bool {
88        if self.in_resource() {
89            return false;
90        }
91
92        let current_type_path = self.current_type_path();
93
94        let is_id = current_type_path.last_split() == Some("id");
95        if is_id {
96            return true;
97        }
98
99        if self.parent_element_is_resource()
100            && current_type_path
101                .last_split()
102                .map(|s| RESOURCE_COMMON_PRIMITIVE_FIELDS.contains(&s))
103                .unwrap_or(false)
104        {
105            return true;
106        }
107
108        type_hints(self.fhir_release)
109            .all_primitives_paths
110            .contains(&self.current_type_path().path)
111    }
112
113    #[inline]
114    pub fn current_element_is_sequence(&self) -> bool {
115        let current_type_path = self.current_type_path();
116
117        let is_common_sequence_field = current_type_path
118            .last_split()
119            .map(|p| COMMON_SEQUENCE_FIELDS.contains(&p))
120            .unwrap_or(false);
121
122        if is_common_sequence_field {
123            return true;
124        }
125
126        let is_contained =
127            current_type_path.len() == 2 && current_type_path.last_split() == Some("contained");
128
129        if is_contained {
130            return true;
131        }
132
133        type_hints(self.fhir_release)
134            .array_paths
135            .contains(&current_type_path.path)
136    }
137
138    #[inline]
139    pub fn current_element_is_boolean(&self) -> bool {
140        type_hints(self.fhir_release)
141            .boolean_paths
142            .contains(&self.current_type_path().path)
143    }
144
145    #[inline]
146    pub fn current_element_is_integer(&self) -> bool {
147        type_hints(self.fhir_release)
148            .integer_paths
149            .contains(&self.current_type_path().path)
150    }
151
152    #[inline]
153    pub fn current_element_is_integer64(&self) -> bool {
154        type_hints(self.fhir_release)
155            .integer64_paths
156            .contains(&self.current_type_path().path)
157    }
158
159    #[inline]
160    pub fn current_element_is_unsigned_integer(&self) -> bool {
161        type_hints(self.fhir_release)
162            .unsigned_integer_paths
163            .contains(&self.current_type_path().path)
164    }
165
166    #[inline]
167    pub fn current_element_is_positive_integer(&self) -> bool {
168        type_hints(self.fhir_release)
169            .positive_integer_paths
170            .contains(&self.current_type_path().path)
171    }
172
173    #[inline]
174    pub fn current_element_is_decimal(&self) -> bool {
175        type_hints(self.fhir_release)
176            .decimal_paths
177            .contains(&self.current_type_path().path)
178    }
179
180    #[inline]
181    pub fn parent_element_is_resource(&self) -> bool {
182        if let Some(path) = self.current_type_path().parent() {
183            path.is_first_letter_uppercase() && !path.contains('.')
184        } else {
185            false
186        }
187    }
188
189    #[inline]
190    pub fn parent_element_is_boolean(&self) -> bool {
191        if let Some(path) = self.current_type_path().parent() {
192            type_hints(self.fhir_release).boolean_paths.contains(path)
193        } else {
194            false
195        }
196    }
197
198    #[inline]
199    pub fn parent_element_is_integer(&self) -> bool {
200        if let Some(path) = self.current_type_path().parent() {
201            type_hints(self.fhir_release).integer_paths.contains(path)
202        } else {
203            false
204        }
205    }
206
207    #[inline]
208    pub fn parent_element_is_integer64(&self) -> bool {
209        if let Some(path) = self.current_type_path().parent() {
210            type_hints(self.fhir_release).integer64_paths.contains(path)
211        } else {
212            false
213        }
214    }
215
216    #[inline]
217    pub fn parent_element_is_unsigned_integer(&self) -> bool {
218        if let Some(path) = self.current_type_path().parent() {
219            type_hints(self.fhir_release)
220                .unsigned_integer_paths
221                .contains(path)
222        } else {
223            false
224        }
225    }
226
227    #[inline]
228    pub fn parent_element_is_positive_integer(&self) -> bool {
229        if let Some(path) = self.current_type_path().parent() {
230            type_hints(self.fhir_release)
231                .positive_integer_paths
232                .contains(path)
233        } else {
234            false
235        }
236    }
237
238    #[inline]
239    pub fn parent_element_is_decimal(&self) -> bool {
240        if let Some(path) = self.current_type_path().parent() {
241            type_hints(self.fhir_release).decimal_paths.contains(path)
242        } else {
243            false
244        }
245    }
246
247    #[inline]
248    pub fn push(&mut self, element: &str) {
249        match self.resolve_current_type() {
250            Some("Resource") => self
251                .type_stack
252                .push(TypePath::new(element, self.fhir_release)),
253            Some(ty) => {
254                let mut type_path = TypePath::new(ty, self.fhir_release);
255                type_path.push(element);
256                self.type_stack.push(type_path);
257            }
258            None => self.type_stack.last_mut().push(element),
259        }
260    }
261
262    #[inline]
263    pub fn pop(&mut self) {
264        self.type_stack.last_mut().pop();
265
266        if self.type_stack.len() > 1
267            && self.type_stack.last().len() <= 1
268            && !self.in_contained_resource()
269        {
270            self.type_stack.pop();
271        }
272    }
273
274    #[inline]
275    pub fn children(&self) -> Option<&'static ElementSet> {
276        let mut type_path = self.current_type_path().path.as_str();
277
278        if let Some(current_type) = self.resolve_current_type() {
279            if current_type != "Resource" {
280                type_path = current_type;
281            }
282        } else if let Some(content_reference) = type_hints(self.fhir_release)
283            .content_reference_paths
284            .get(type_path)
285        {
286            type_path = content_reference;
287        }
288
289        element_map(self.fhir_release).get(type_path).copied()
290    }
291
292    #[inline]
293    pub fn position_of_child(&self, child: &str) -> usize {
294        if child == "resourceType"
295            // on R4 ExampleScenario.instance contains a field named "resourceType"
296            && !self.current_type_path().path.starts_with("ExampleScenario.instance")
297            // on R5 Consent.provision contains a field named "resourceType"
298            && !self.current_type_path().path.starts_with("Consent.provision")
299            // on R5 Subscription.filterBy contains a field named "resourceType"
300            && !self.current_type_path().path.starts_with("Subscription.filterBy")
301        {
302            0
303        } else {
304            self.children()
305                .and_then(|set| set.get_index(child))
306                .map(|i| i + 1)
307                // move unknown to the end
308                .unwrap_or(usize::MAX)
309        }
310    }
311
312    fn current_type_path(&self) -> &TypePath {
313        self.type_stack.last()
314    }
315
316    fn resolve_current_type(&self) -> Option<&str> {
317        if self.current_element_is_extension() {
318            return Some("Extension");
319        }
320
321        let current_type_path = self.current_type_path();
322
323        if current_type_path.len() == 2 {
324            match current_type_path.last_split() {
325                Some("meta") => return Some("Meta"),
326                Some("text") => return Some("Narrative"),
327                Some("contained") => return Some("Resource"),
328                _ => (),
329            }
330        }
331
332        type_hints(self.fhir_release)
333            .type_paths
334            .get(&current_type_path.path)
335            .copied()
336    }
337
338    fn in_resource(&self) -> bool {
339        let current_type_path = self.current_type_path();
340
341        current_type_path.len() == 1 && current_type_path.path.as_str().is_first_letter_uppercase()
342    }
343
344    fn in_contained_resource(&self) -> bool {
345        if !self.in_resource() {
346            return false;
347        }
348
349        let previous_type_path = if let Some(previous) = self.type_stack.second_last() {
350            previous
351        } else {
352            return false;
353        };
354
355        let in_contained_field = previous_type_path.last_split() == Some("contained");
356
357        if in_contained_field {
358            return true;
359        }
360
361        type_hints(self.fhir_release)
362            .type_paths
363            .get(&previous_type_path.path)
364            == Some(&"Resource")
365    }
366}
367
368#[derive(Debug, Clone)]
369struct TypeStack {
370    root: TypePath,
371    stack: Vec<TypePath>,
372}
373
374impl TypeStack {
375    fn new(fhir_release: FhirRelease) -> TypeStack {
376        TypeStack {
377            root: TypePath::empty(fhir_release),
378            stack: vec![],
379        }
380    }
381
382    fn push(&mut self, value: TypePath) {
383        self.stack.push(value)
384    }
385
386    fn pop(&mut self) {
387        self.stack.pop();
388    }
389
390    fn is_empty(&self) -> bool {
391        self.stack.is_empty() && self.root.is_empty()
392    }
393
394    fn len(&self) -> usize {
395        self.stack.len() + 1
396    }
397
398    fn last(&self) -> &TypePath {
399        if let Some(last) = self.stack.last() {
400            last
401        } else {
402            &self.root
403        }
404    }
405
406    fn last_mut(&mut self) -> &mut TypePath {
407        if let Some(last) = self.stack.last_mut() {
408            last
409        } else {
410            &mut self.root
411        }
412    }
413
414    fn second_last(&self) -> Option<&TypePath> {
415        match self.stack.as_slice() {
416            [_] => Some(&self.root),
417            [.., second_last, _] => Some(second_last),
418            _ => None,
419        }
420    }
421}
422
423#[derive(Debug, Clone)]
424struct TypePath {
425    fhir_release: FhirRelease,
426    path: String,
427    content_reference_replacement_stack: Vec<ContentReferenceReplacement>,
428}
429
430#[derive(Debug, Clone)]
431struct ContentReferenceReplacement {
432    content_reference: &'static str,
433    replaced: String,
434}
435
436impl TypePath {
437    fn new(typ_name: &str, fhir_release: FhirRelease) -> TypePath {
438        TypePath {
439            fhir_release,
440            path: typ_name.to_string(),
441            content_reference_replacement_stack: vec![],
442        }
443    }
444
445    fn empty(fhir_release: FhirRelease) -> TypePath {
446        TypePath {
447            fhir_release,
448            path: String::new(),
449            content_reference_replacement_stack: vec![],
450        }
451    }
452
453    fn parent(&self) -> Option<&str> {
454        self.path.rsplit_once('.').map(|s| s.0)
455    }
456
457    fn last_split(&self) -> Option<&str> {
458        self.path.rsplit_once('.').map(|s| s.1)
459    }
460
461    fn is_empty(&self) -> bool {
462        self.len() == 0
463    }
464
465    fn len(&self) -> usize {
466        if self.path.is_empty() {
467            0
468        } else {
469            1 + self.path.chars().filter(|c| *c == '.').count()
470        }
471    }
472
473    fn push(&mut self, element: &str) {
474        if let Some(content_reference) = type_hints(self.fhir_release)
475            .content_reference_paths
476            .get(&self.path)
477        {
478            self.content_reference_replacement_stack
479                .push(ContentReferenceReplacement {
480                    content_reference,
481                    replaced: mem::replace(&mut self.path, content_reference.to_string()),
482                })
483        }
484
485        if !self.path.is_empty() {
486            self.path.push('.');
487        }
488        self.path.push_str(element);
489    }
490
491    fn pop(&mut self) {
492        self.path.truncate(self.path.rfind('.').unwrap_or(0));
493
494        let last_replacement = match self.content_reference_replacement_stack.last_mut() {
495            Some(last_replacement) => last_replacement,
496            None => return,
497        };
498
499        if last_replacement.content_reference == self.path {
500            self.path = mem::take(&mut last_replacement.replaced);
501
502            self.content_reference_replacement_stack.pop();
503        };
504    }
505}