Skip to main content

rh_foundation/snapshot/
path.rs

1#[derive(Debug, Clone, PartialEq, Eq)]
2pub struct ElementPath {
3    parts: Vec<String>,
4    original: String,
5}
6
7impl ElementPath {
8    pub fn new(path: &str) -> Self {
9        let parts: Vec<String> = path.split('.').map(|s| s.to_string()).collect();
10        Self {
11            parts,
12            original: path.to_string(),
13        }
14    }
15
16    /// Constructs an `ElementPath` from a vector of parts.
17    ///
18    /// This is more efficient than `new()` when you already have the parts vector,
19    /// as it avoids the split operation. Useful when converting from `parent()` results.
20    ///
21    /// # Example
22    ///
23    /// ```
24    /// use rh_foundation::snapshot::ElementPath;
25    ///
26    /// let path = ElementPath::new("Patient.name.given");
27    /// let parent_parts = path.parent().unwrap();
28    /// let parent_path = ElementPath::from_parts(parent_parts.to_vec());
29    /// assert_eq!(parent_path.as_str(), "Patient.name");
30    /// ```
31    pub fn from_parts(parts: Vec<String>) -> Self {
32        let original = parts.join(".");
33        Self { parts, original }
34    }
35
36    pub fn parts(&self) -> &[String] {
37        &self.parts
38    }
39
40    pub fn as_str(&self) -> &str {
41        &self.original
42    }
43
44    pub fn depth(&self) -> usize {
45        self.parts.len()
46    }
47
48    pub fn is_parent_of(&self, other: &ElementPath) -> bool {
49        if self.depth() >= other.depth() {
50            return false;
51        }
52
53        for (i, part) in self.parts.iter().enumerate() {
54            if other.parts.get(i) != Some(part) {
55                return false;
56            }
57        }
58
59        true
60    }
61
62    pub fn is_child_of(&self, other: &ElementPath) -> bool {
63        other.is_parent_of(self)
64    }
65
66    pub fn is_immediate_child_of(&self, parent: &ElementPath) -> bool {
67        self.depth() == parent.depth() + 1 && self.is_child_of(parent)
68    }
69
70    /// Returns a slice view of the parent path's parts.
71    ///
72    /// This is a zero-allocation operation that returns a borrowed slice.
73    /// If you need an `ElementPath` instance, use `ElementPath::from_parts(parent.to_vec())`.
74    ///
75    /// # Returns
76    ///
77    /// - `Some(&[String])` - A slice containing the parent's path parts
78    /// - `None` - If this is a root element (depth <= 1)
79    ///
80    /// # Example
81    ///
82    /// ```
83    /// use rh_foundation::snapshot::ElementPath;
84    ///
85    /// let path = ElementPath::new("Patient.name.given");
86    /// let parent_parts = path.parent().unwrap();
87    /// assert_eq!(parent_parts, &["Patient", "name"]);
88    ///
89    /// // Convert to ElementPath if needed
90    /// let parent_path = ElementPath::from_parts(parent_parts.to_vec());
91    /// assert_eq!(parent_path.as_str(), "Patient.name");
92    /// ```
93    pub fn parent(&self) -> Option<&[String]> {
94        if self.parts.len() <= 1 {
95            return None;
96        }
97
98        Some(&self.parts[0..self.parts.len() - 1])
99    }
100
101    pub fn matches_choice_type(&self, base_path: &ElementPath) -> bool {
102        if self.depth() != base_path.depth() {
103            return false;
104        }
105
106        for i in 0..self.parts.len() - 1 {
107            if self.parts[i] != base_path.parts[i] {
108                return false;
109            }
110        }
111
112        let base_last = base_path.parts.last().unwrap();
113        let self_last = self.parts.last().unwrap();
114
115        if base_last.ends_with("[x]") {
116            let base_prefix = &base_last[..base_last.len() - 3];
117            self_last.starts_with(base_prefix)
118        } else {
119            false
120        }
121    }
122
123    fn is_lowercase_start(s: &str) -> bool {
124        s.chars().next().is_some_and(|c| c.is_lowercase())
125    }
126
127    pub fn normalize_choice_type(&self) -> ElementPath {
128        let mut normalized_parts = self.parts.clone();
129        if let Some(last) = normalized_parts.last_mut() {
130            if last.len() > 3 && Self::is_lowercase_start(last) {
131                for (i, c) in last.char_indices() {
132                    if c.is_uppercase() {
133                        let prefix = &last[..i];
134                        *last = format!("{prefix}[x]");
135                        break;
136                    }
137                }
138            }
139        }
140        Self::from_parts(normalized_parts)
141    }
142
143    pub fn is_slice(&self) -> bool {
144        self.original.contains(':')
145    }
146
147    pub fn slice_name(&self) -> Option<&str> {
148        for part in &self.parts {
149            if let Some(colon_pos) = part.rfind(':') {
150                return Some(&part[colon_pos + 1..]);
151            }
152        }
153        None
154    }
155
156    pub fn base_path(&self) -> ElementPath {
157        if !self.is_slice() {
158            return self.clone();
159        }
160
161        let base_parts: Vec<String> = self
162            .parts
163            .iter()
164            .map(|part| {
165                if let Some(colon_pos) = part.rfind(':') {
166                    part[..colon_pos].to_string()
167                } else {
168                    part.clone()
169                }
170            })
171            .collect();
172
173        Self::from_parts(base_parts)
174    }
175
176    pub fn is_reslice(&self) -> bool {
177        if let Some(last_part) = self.parts.last() {
178            last_part.matches(':').count() > 1
179        } else {
180            false
181        }
182    }
183
184    pub fn parent_slice(&self) -> Option<ElementPath> {
185        if !self.is_reslice() {
186            return None;
187        }
188
189        if let Some(last_part) = self.parts.last() {
190            let colon_pos = last_part.rfind(':').unwrap();
191            let mut parent_parts = self.parts.clone();
192            parent_parts.pop();
193            parent_parts.push(last_part[..colon_pos].to_string());
194            return Some(Self::from_parts(parent_parts));
195        }
196
197        None
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_path_parsing() {
207        let path = ElementPath::new("Patient.name.given");
208        assert_eq!(path.parts(), &["Patient", "name", "given"]);
209        assert_eq!(path.as_str(), "Patient.name.given");
210        assert_eq!(path.depth(), 3);
211    }
212
213    #[test]
214    fn test_single_part_path() {
215        let path = ElementPath::new("Patient");
216        assert_eq!(path.parts(), &["Patient"]);
217        assert_eq!(path.depth(), 1);
218    }
219
220    #[test]
221    fn test_is_parent_of() {
222        let parent = ElementPath::new("Patient.name");
223        let child = ElementPath::new("Patient.name.given");
224        let not_child = ElementPath::new("Patient.identifier");
225
226        assert!(parent.is_parent_of(&child));
227        assert!(!parent.is_parent_of(&not_child));
228        assert!(!parent.is_parent_of(&parent));
229    }
230
231    #[test]
232    fn test_is_child_of() {
233        let parent = ElementPath::new("Patient.name");
234        let child = ElementPath::new("Patient.name.given");
235
236        assert!(child.is_child_of(&parent));
237        assert!(!parent.is_child_of(&child));
238    }
239
240    #[test]
241    fn test_is_immediate_child_of() {
242        let parent = ElementPath::new("Patient.name");
243        let immediate_child = ElementPath::new("Patient.name.given");
244        let grandchild = ElementPath::new("Patient.name.given.extension");
245
246        assert!(immediate_child.is_immediate_child_of(&parent));
247        assert!(!grandchild.is_immediate_child_of(&parent));
248        assert!(!parent.is_immediate_child_of(&immediate_child));
249    }
250
251    #[test]
252    fn test_parent() {
253        let path = ElementPath::new("Patient.name.given");
254        let parent_parts = path.parent().unwrap();
255        assert_eq!(parent_parts, &["Patient", "name"]);
256
257        let root = ElementPath::new("Patient");
258        assert!(root.parent().is_none());
259    }
260
261    #[test]
262    fn test_matches_choice_type() {
263        let base = ElementPath::new("Observation.value[x]");
264        let string_variant = ElementPath::new("Observation.valueString");
265        let quantity_variant = ElementPath::new("Observation.valueQuantity");
266        let codeable_variant = ElementPath::new("Observation.valueCodeableConcept");
267        let other = ElementPath::new("Observation.status");
268
269        assert!(string_variant.matches_choice_type(&base));
270        assert!(quantity_variant.matches_choice_type(&base));
271        assert!(codeable_variant.matches_choice_type(&base));
272        assert!(!other.matches_choice_type(&base));
273    }
274
275    #[test]
276    fn test_normalize_choice_type() {
277        let string_path = ElementPath::new("Observation.valueString");
278        let normalized = string_path.normalize_choice_type();
279        assert_eq!(normalized.as_str(), "Observation.value[x]");
280
281        let quantity_path = ElementPath::new("Observation.valueQuantity");
282        let normalized = quantity_path.normalize_choice_type();
283        assert_eq!(normalized.as_str(), "Observation.value[x]");
284
285        let codeable_path = ElementPath::new("Observation.valueCodeableConcept");
286        let normalized = codeable_path.normalize_choice_type();
287        assert_eq!(normalized.as_str(), "Observation.value[x]");
288    }
289
290    #[test]
291    fn test_normalize_non_choice_type() {
292        let normal_path = ElementPath::new("Patient.name");
293        let normalized = normal_path.normalize_choice_type();
294        assert_eq!(normalized.as_str(), "Patient.name");
295    }
296
297    #[test]
298    fn test_multi_level_parent_child() {
299        let root = ElementPath::new("Patient");
300        let level1 = ElementPath::new("Patient.name");
301        let level2 = ElementPath::new("Patient.name.given");
302        let level3 = ElementPath::new("Patient.name.given.extension");
303
304        assert!(root.is_parent_of(&level1));
305        assert!(root.is_parent_of(&level2));
306        assert!(root.is_parent_of(&level3));
307
308        assert!(level1.is_parent_of(&level2));
309        assert!(level1.is_parent_of(&level3));
310
311        assert!(level2.is_parent_of(&level3));
312    }
313
314    #[test]
315    fn test_parent_chain() {
316        let path = ElementPath::new("Patient.name.given.extension");
317
318        // Test single parent access
319        let parent1 = path.parent().unwrap();
320        assert_eq!(parent1, &["Patient", "name", "given"]);
321
322        // For chaining, convert back to ElementPath
323        let parent1_path = ElementPath::from_parts(parent1.to_vec());
324        let parent2 = parent1_path.parent().unwrap();
325        assert_eq!(parent2, &["Patient", "name"]);
326
327        let parent2_path = ElementPath::from_parts(parent2.to_vec());
328        let parent3 = parent2_path.parent().unwrap();
329        assert_eq!(parent3, &["Patient"]);
330
331        let parent3_path = ElementPath::from_parts(parent3.to_vec());
332        assert!(parent3_path.parent().is_none());
333    }
334
335    #[test]
336    fn test_is_slice() {
337        let slice_path = ElementPath::new("Patient.identifier:MRN");
338        let normal_path = ElementPath::new("Patient.identifier");
339
340        assert!(slice_path.is_slice());
341        assert!(!normal_path.is_slice());
342    }
343
344    #[test]
345    fn test_slice_name() {
346        let slice_path = ElementPath::new("Patient.identifier:MRN");
347        assert_eq!(slice_path.slice_name(), Some("MRN"));
348
349        let normal_path = ElementPath::new("Patient.identifier");
350        assert_eq!(normal_path.slice_name(), None);
351
352        let nested_slice = ElementPath::new("Patient.identifier:MRN.system");
353        assert_eq!(nested_slice.slice_name(), Some("MRN"));
354    }
355
356    #[test]
357    fn test_base_path() {
358        let slice_path = ElementPath::new("Patient.identifier:MRN");
359        let base = slice_path.base_path();
360        assert_eq!(base.as_str(), "Patient.identifier");
361
362        let normal_path = ElementPath::new("Patient.identifier");
363        let base_normal = normal_path.base_path();
364        assert_eq!(base_normal.as_str(), "Patient.identifier");
365    }
366
367    #[test]
368    fn test_is_reslice() {
369        let reslice_path = ElementPath::new("Patient.identifier:MRN:subslice");
370        let slice_path = ElementPath::new("Patient.identifier:MRN");
371        let normal_path = ElementPath::new("Patient.identifier");
372
373        assert!(reslice_path.is_reslice());
374        assert!(!slice_path.is_reslice());
375        assert!(!normal_path.is_reslice());
376    }
377
378    #[test]
379    fn test_parent_slice() {
380        let reslice_path = ElementPath::new("Patient.identifier:MRN:subslice");
381        let parent = reslice_path.parent_slice().unwrap();
382        assert_eq!(parent.as_str(), "Patient.identifier:MRN");
383
384        let slice_path = ElementPath::new("Patient.identifier:MRN");
385        assert!(slice_path.parent_slice().is_none());
386    }
387
388    #[test]
389    fn test_slice_with_children() {
390        let slice_child = ElementPath::new("Patient.identifier:MRN.system");
391        assert!(slice_child.is_slice());
392        assert_eq!(slice_child.slice_name(), Some("MRN"));
393
394        let base = slice_child.base_path();
395        assert_eq!(base.as_str(), "Patient.identifier.system");
396    }
397
398    #[test]
399    fn test_is_lowercase_start() {
400        assert!(ElementPath::is_lowercase_start("abc"));
401        assert!(!ElementPath::is_lowercase_start("Abc"));
402        assert!(!ElementPath::is_lowercase_start(""));
403        assert!(!ElementPath::is_lowercase_start("123"));
404        assert!(!ElementPath::is_lowercase_start("ABC"));
405    }
406
407    #[test]
408    fn test_normalize_choice_type_minimum_length() {
409        // "valA" -> len 4. >3. 'v' is lowercase. 'A' is upper. -> "val[x]"
410        let path = ElementPath::new("Observation.valA");
411        let normalized = path.normalize_choice_type();
412        assert_eq!(normalized.as_str(), "Observation.val[x]");
413
414        // "vaA" -> len 3. Not >3. -> "vaA"
415        let short_path = ElementPath::new("Observation.vaA");
416        let short_normalized = short_path.normalize_choice_type();
417        assert_eq!(short_normalized.as_str(), "Observation.vaA");
418    }
419}