domainstack/
path.rs

1use std::sync::Arc;
2
3/// Represents a path to a field in a nested structure.
4///
5/// Paths are used to identify which field caused a validation error in nested
6/// and collection structures. They support dot notation for nested fields and
7/// bracket notation for array indices.
8///
9/// # Examples
10///
11/// ```
12/// use domainstack::Path;
13///
14/// // Simple field path
15/// let path = Path::root().field("email");
16/// assert_eq!(path.to_string(), "email");
17///
18/// // Nested path
19/// let path = Path::root().field("user").field("email");
20/// assert_eq!(path.to_string(), "user.email");
21///
22/// // Collection path
23/// let path = Path::root().field("items").index(0).field("name");
24/// assert_eq!(path.to_string(), "items[0].name");
25/// ```
26///
27/// # Memory Management
28///
29/// Path uses `Arc<str>` for field names, providing:
30/// - **No memory leaks:** Reference counting ensures proper cleanup
31/// - **Efficient cloning:** Cloning a path is cheap (just incrementing reference counts)
32/// - **Shared ownership:** Multiple errors can reference the same field names
33///
34/// Field names from compile-time literals (`"email"`) are converted to `Arc<str>`
35/// on first use and reference-counted thereafter.
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub struct Path(Vec<PathSegment>);
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub enum PathSegment {
41    Field(Arc<str>),
42    Index(usize),
43}
44
45impl Path {
46    /// Creates an empty root path.
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use domainstack::Path;
52    ///
53    /// let path = Path::root();
54    /// assert_eq!(path.to_string(), "");
55    /// ```
56    pub fn root() -> Self {
57        Self(Vec::new())
58    }
59
60    /// Appends a field name to the path.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use domainstack::Path;
66    ///
67    /// let path = Path::root().field("email");
68    /// assert_eq!(path.to_string(), "email");
69    ///
70    /// let nested = Path::root().field("user").field("email");
71    /// assert_eq!(nested.to_string(), "user.email");
72    /// ```
73    pub fn field(mut self, name: impl Into<Arc<str>>) -> Self {
74        self.0.push(PathSegment::Field(name.into()));
75        self
76    }
77
78    /// Appends an array index to the path.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use domainstack::Path;
84    ///
85    /// let path = Path::root().field("items").index(0);
86    /// assert_eq!(path.to_string(), "items[0]");
87    ///
88    /// let nested = Path::root().field("items").index(0).field("name");
89    /// assert_eq!(nested.to_string(), "items[0].name");
90    /// ```
91    pub fn index(mut self, idx: usize) -> Self {
92        self.0.push(PathSegment::Index(idx));
93        self
94    }
95
96    /// Parses a path from a string representation.
97    ///
98    /// Uses `Arc<str>` for field names, ensuring proper memory management
99    /// without leaks. Field names are reference-counted and cleaned up
100    /// when no longer needed.
101    ///
102    /// # Examples
103    ///
104    /// ```
105    /// use domainstack::Path;
106    ///
107    /// let path = Path::parse("user.email");
108    /// assert_eq!(path, Path::root().field("user").field("email"));
109    ///
110    /// let with_index = Path::parse("items[0].name");
111    /// assert_eq!(with_index, Path::root().field("items").index(0).field("name"));
112    /// ```
113    pub fn parse(s: &str) -> Self {
114        let mut segments = Vec::new();
115        let mut current = String::new();
116
117        let chars: Vec<char> = s.chars().collect();
118        let mut i = 0;
119
120        while i < chars.len() {
121            match chars[i] {
122                '.' => {
123                    if !current.is_empty() {
124                        segments.push(PathSegment::Field(Arc::from(current.as_str())));
125                        current.clear();
126                    }
127                    i += 1;
128                }
129                '[' => {
130                    if !current.is_empty() {
131                        segments.push(PathSegment::Field(Arc::from(current.as_str())));
132                        current.clear();
133                    }
134
135                    i += 1;
136                    let mut index_str = String::new();
137                    while i < chars.len() && chars[i] != ']' {
138                        index_str.push(chars[i]);
139                        i += 1;
140                    }
141
142                    if let Ok(idx) = index_str.parse::<usize>() {
143                        segments.push(PathSegment::Index(idx));
144                    }
145
146                    i += 1;
147                }
148                _ => {
149                    current.push(chars[i]);
150                    i += 1;
151                }
152            }
153        }
154
155        if !current.is_empty() {
156            segments.push(PathSegment::Field(Arc::from(current.as_str())));
157        }
158
159        Path(segments)
160    }
161
162    /// Returns a slice of the path segments.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use domainstack::{Path, PathSegment};
168    ///
169    /// let path = Path::root().field("user").index(0).field("name");
170    /// assert_eq!(path.segments().len(), 3);
171    /// ```
172    pub fn segments(&self) -> &[PathSegment] {
173        &self.0
174    }
175
176    /// Pushes a field segment to the path.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use domainstack::Path;
182    ///
183    /// let mut path = Path::root();
184    /// path.push_field("email");
185    /// assert_eq!(path.to_string(), "email");
186    /// ```
187    pub fn push_field(&mut self, name: impl Into<Arc<str>>) {
188        self.0.push(PathSegment::Field(name.into()));
189    }
190
191    /// Pushes an index segment to the path.
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use domainstack::Path;
197    ///
198    /// let mut path = Path::root();
199    /// path.push_field("items");
200    /// path.push_index(0);
201    /// assert_eq!(path.to_string(), "items[0]");
202    /// ```
203    pub fn push_index(&mut self, idx: usize) {
204        self.0.push(PathSegment::Index(idx));
205    }
206}
207
208impl core::fmt::Display for Path {
209    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
210        for (i, segment) in self.0.iter().enumerate() {
211            match segment {
212                PathSegment::Field(name) => {
213                    if i > 0 {
214                        write!(f, ".")?;
215                    }
216                    write!(f, "{}", name)?;
217                }
218                PathSegment::Index(idx) => write!(f, "[{}]", idx)?,
219            }
220        }
221        Ok(())
222    }
223}
224
225impl From<&'static str> for Path {
226    fn from(s: &'static str) -> Self {
227        Path(vec![PathSegment::Field(Arc::from(s))])
228    }
229}
230
231impl From<String> for Path {
232    fn from(s: String) -> Self {
233        Path::parse(&s)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_root() {
243        let path = Path::root();
244        assert!(path.segments().is_empty());
245        assert_eq!(path.to_string(), "");
246    }
247
248    #[test]
249    fn test_field() {
250        let path = Path::root().field("email");
251        assert_eq!(path.segments().len(), 1);
252        assert_eq!(path.to_string(), "email");
253    }
254
255    #[test]
256    fn test_nested_field() {
257        let path = Path::root().field("guest").field("email");
258        assert_eq!(path.segments().len(), 2);
259        assert_eq!(path.to_string(), "guest.email");
260    }
261
262    #[test]
263    fn test_index() {
264        let path = Path::root().field("guests").index(0);
265        assert_eq!(path.segments().len(), 2);
266        assert_eq!(path.to_string(), "guests[0]");
267    }
268
269    #[test]
270    fn test_complex_path() {
271        let path = Path::root()
272            .field("booking")
273            .field("guests")
274            .index(0)
275            .field("email");
276        assert_eq!(path.to_string(), "booking.guests[0].email");
277    }
278
279    #[test]
280    fn test_from_str() {
281        let path = Path::from("email");
282        assert_eq!(path.segments().len(), 1);
283        assert_eq!(path.to_string(), "email");
284    }
285
286    #[test]
287    fn test_parse_simple() {
288        let path = Path::parse("email");
289        assert_eq!(path.to_string(), "email");
290    }
291
292    #[test]
293    fn test_parse_nested() {
294        let path = Path::parse("guest.email");
295        assert_eq!(path.to_string(), "guest.email");
296    }
297
298    #[test]
299    fn test_parse_with_index() {
300        let path = Path::parse("guests[0].email");
301        assert_eq!(path.to_string(), "guests[0].email");
302    }
303
304    #[test]
305    fn test_parse_complex() {
306        let path = Path::parse("booking.guests[0].email");
307        assert_eq!(path.to_string(), "booking.guests[0].email");
308    }
309
310    // Tests for push methods
311    #[test]
312    fn test_push_field_basic() {
313        let mut path = Path::root();
314        path.push_field("email");
315        assert_eq!(path.segments().len(), 1);
316        assert_eq!(path.to_string(), "email");
317    }
318
319    #[test]
320    fn test_push_field_multiple() {
321        let mut path = Path::root();
322        path.push_field("user");
323        path.push_field("profile");
324        path.push_field("email");
325        assert_eq!(path.segments().len(), 3);
326        assert_eq!(path.to_string(), "user.profile.email");
327    }
328
329    #[test]
330    fn test_push_index_basic() {
331        let mut path = Path::root();
332        path.push_field("items");
333        path.push_index(0);
334        assert_eq!(path.segments().len(), 2);
335        assert_eq!(path.to_string(), "items[0]");
336    }
337
338    #[test]
339    fn test_push_index_multiple() {
340        let mut path = Path::root();
341        path.push_field("matrix");
342        path.push_index(0);
343        path.push_index(1);
344        assert_eq!(path.segments().len(), 3);
345        assert_eq!(path.to_string(), "matrix[0][1]");
346    }
347
348    #[test]
349    fn test_push_field_and_index_mixed() {
350        let mut path = Path::root();
351        path.push_field("orders");
352        path.push_index(5);
353        path.push_field("items");
354        path.push_index(3);
355        path.push_field("sku");
356        assert_eq!(path.segments().len(), 5);
357        assert_eq!(path.to_string(), "orders[5].items[3].sku");
358    }
359
360    #[test]
361    fn test_push_with_string() {
362        let mut path = Path::root();
363        path.push_field(String::from("dynamic_field"));
364        assert_eq!(path.to_string(), "dynamic_field");
365    }
366
367    // Parse edge cases
368    #[test]
369    fn test_parse_empty_string() {
370        let path = Path::parse("");
371        assert!(path.segments().is_empty());
372        assert_eq!(path.to_string(), "");
373    }
374
375    #[test]
376    fn test_parse_leading_dot() {
377        let path = Path::parse(".field");
378        // Leading dot should be skipped, resulting in just "field"
379        assert_eq!(path.to_string(), "field");
380    }
381
382    #[test]
383    fn test_parse_trailing_dot() {
384        let path = Path::parse("field.");
385        // Trailing dot is ignored
386        assert_eq!(path.to_string(), "field");
387    }
388
389    #[test]
390    fn test_parse_consecutive_dots() {
391        let path = Path::parse("a..b");
392        // Empty segments between dots are skipped
393        assert_eq!(path.to_string(), "a.b");
394    }
395
396    #[test]
397    fn test_parse_consecutive_indices() {
398        let path = Path::parse("items[0][1][2]");
399        assert_eq!(path.segments().len(), 4);
400        assert_eq!(path.to_string(), "items[0][1][2]");
401    }
402
403    #[test]
404    fn test_parse_invalid_index_ignored() {
405        let path = Path::parse("items[abc]");
406        // Non-numeric index is ignored, only field is captured
407        assert_eq!(path.segments().len(), 1);
408        assert_eq!(path.to_string(), "items");
409    }
410
411    #[test]
412    fn test_parse_negative_index_ignored() {
413        let path = Path::parse("items[-1]");
414        // Negative index can't be parsed as usize, ignored
415        assert_eq!(path.segments().len(), 1);
416        assert_eq!(path.to_string(), "items");
417    }
418
419    #[test]
420    fn test_parse_unclosed_bracket() {
421        let path = Path::parse("items[0");
422        // Unclosed bracket - index still captured
423        assert_eq!(path.segments().len(), 2);
424        assert_eq!(path.to_string(), "items[0]");
425    }
426
427    #[test]
428    fn test_parse_deep_nesting() {
429        let path = Path::parse("a.b.c.d.e.f.g.h.i.j.k");
430        assert_eq!(path.segments().len(), 11);
431        assert_eq!(path.to_string(), "a.b.c.d.e.f.g.h.i.j.k");
432    }
433
434    #[test]
435    fn test_parse_deep_mixed_nesting() {
436        let path = Path::parse("a[0].b[1].c[2].d[3].e[4]");
437        assert_eq!(path.segments().len(), 10);
438        assert_eq!(path.to_string(), "a[0].b[1].c[2].d[3].e[4]");
439    }
440
441    #[test]
442    fn test_parse_large_index() {
443        let path = Path::parse("items[999999]");
444        assert_eq!(path.to_string(), "items[999999]");
445    }
446
447    // Equality and hashing tests
448    #[test]
449    fn test_parsed_equals_constructed() {
450        let parsed = Path::parse("user.items[0].name");
451        let constructed = Path::root()
452            .field("user")
453            .field("items")
454            .index(0)
455            .field("name");
456        assert_eq!(parsed, constructed);
457    }
458
459    #[test]
460    fn test_path_hash_consistency() {
461        use std::collections::HashMap;
462
463        let path1 = Path::parse("user.email");
464        let path2 = Path::root().field("user").field("email");
465
466        let mut map = HashMap::new();
467        map.insert(path1.clone(), "value1");
468
469        // Same path constructed differently should access same entry
470        assert_eq!(map.get(&path2), Some(&"value1"));
471    }
472
473    #[test]
474    fn test_clone_independence() {
475        let original = Path::root().field("test");
476        let mut cloned = original.clone();
477        cloned.push_field("extra");
478
479        assert_eq!(original.segments().len(), 1);
480        assert_eq!(cloned.segments().len(), 2);
481        assert_eq!(original.to_string(), "test");
482        assert_eq!(cloned.to_string(), "test.extra");
483    }
484
485    #[test]
486    fn test_from_string() {
487        let path: Path = String::from("user.email").into();
488        assert_eq!(path.to_string(), "user.email");
489    }
490
491    #[test]
492    fn test_segment_types() {
493        let path = Path::root().field("items").index(0).field("name");
494        let segments = path.segments();
495
496        assert!(matches!(&segments[0], PathSegment::Field(_)));
497        assert!(matches!(&segments[1], PathSegment::Index(0)));
498        assert!(matches!(&segments[2], PathSegment::Field(_)));
499    }
500}