Skip to main content

rh_foundation/snapshot/
path.rs

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