postmortem/
path.rs

1//! JSON path representation for locating values in nested structures.
2//!
3//! This module provides [`JsonPath`] and [`PathSegment`] types for building
4//! and representing paths to values in nested JSON-like structures.
5
6use std::fmt::{self, Display};
7
8/// A segment of a JSON path.
9///
10/// Paths are built from segments that represent either field access or array indexing.
11#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12pub enum PathSegment {
13    /// A field/property access (e.g., `user`, `email`)
14    Field(String),
15    /// An array index access (e.g., `[0]`, `[42]`)
16    Index(usize),
17}
18
19impl PathSegment {
20    /// Creates a new field segment.
21    pub fn field(name: impl Into<String>) -> Self {
22        PathSegment::Field(name.into())
23    }
24
25    /// Creates a new index segment.
26    pub fn index(idx: usize) -> Self {
27        PathSegment::Index(idx)
28    }
29}
30
31/// A path to a value in a nested JSON-like structure.
32///
33/// `JsonPath` represents locations like `users[0].email` and provides
34/// methods for building paths incrementally.
35///
36/// # Example
37///
38/// ```rust
39/// use postmortem::JsonPath;
40///
41/// let path = JsonPath::root()
42///     .push_field("users")
43///     .push_index(0)
44///     .push_field("email");
45///
46/// assert_eq!(path.to_string(), "users[0].email");
47/// ```
48#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
49pub struct JsonPath {
50    segments: Vec<PathSegment>,
51}
52
53impl JsonPath {
54    /// Creates an empty path representing the root value.
55    pub fn root() -> Self {
56        Self::default()
57    }
58
59    /// Creates a path from a single field segment.
60    pub fn from_field(name: impl Into<String>) -> Self {
61        Self {
62            segments: vec![PathSegment::Field(name.into())],
63        }
64    }
65
66    /// Creates a path from a single index segment.
67    pub fn from_index(idx: usize) -> Self {
68        Self {
69            segments: vec![PathSegment::Index(idx)],
70        }
71    }
72
73    /// Returns a new path with a field segment appended.
74    ///
75    /// This method does not modify the original path; it returns a new one.
76    pub fn push_field(&self, name: impl Into<String>) -> Self {
77        let mut segments = self.segments.clone();
78        segments.push(PathSegment::Field(name.into()));
79        Self { segments }
80    }
81
82    /// Returns a new path with an index segment appended.
83    ///
84    /// This method does not modify the original path; it returns a new one.
85    pub fn push_index(&self, index: usize) -> Self {
86        let mut segments = self.segments.clone();
87        segments.push(PathSegment::Index(index));
88        Self { segments }
89    }
90
91    /// Returns true if this is the root path (no segments).
92    pub fn is_root(&self) -> bool {
93        self.segments.is_empty()
94    }
95
96    /// Returns the number of segments in this path.
97    pub fn len(&self) -> usize {
98        self.segments.len()
99    }
100
101    /// Returns true if this path has no segments.
102    pub fn is_empty(&self) -> bool {
103        self.segments.is_empty()
104    }
105
106    /// Returns an iterator over the path segments.
107    pub fn segments(&self) -> impl Iterator<Item = &PathSegment> {
108        self.segments.iter()
109    }
110
111    /// Returns the parent path (all segments except the last), or None if this is root.
112    pub fn parent(&self) -> Option<Self> {
113        if self.segments.is_empty() {
114            None
115        } else {
116            Some(Self {
117                segments: self.segments[..self.segments.len() - 1].to_vec(),
118            })
119        }
120    }
121
122    /// Returns the last segment, or None if this is root.
123    pub fn last(&self) -> Option<&PathSegment> {
124        self.segments.last()
125    }
126}
127
128impl Display for JsonPath {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        for (i, segment) in self.segments.iter().enumerate() {
131            match segment {
132                PathSegment::Field(name) => {
133                    if i > 0 {
134                        write!(f, ".")?;
135                    }
136                    write!(f, "{}", name)?;
137                }
138                PathSegment::Index(idx) => write!(f, "[{}]", idx)?,
139            }
140        }
141        Ok(())
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_root_path_is_empty() {
151        let path = JsonPath::root();
152        assert!(path.is_root());
153        assert!(path.is_empty());
154        assert_eq!(path.len(), 0);
155        assert_eq!(path.to_string(), "");
156    }
157
158    #[test]
159    fn test_single_field() {
160        let path = JsonPath::root().push_field("user");
161        assert_eq!(path.to_string(), "user");
162        assert_eq!(path.len(), 1);
163    }
164
165    #[test]
166    fn test_single_index() {
167        let path = JsonPath::root().push_index(0);
168        assert_eq!(path.to_string(), "[0]");
169    }
170
171    #[test]
172    fn test_nested_fields() {
173        let path = JsonPath::root().push_field("user").push_field("email");
174        assert_eq!(path.to_string(), "user.email");
175    }
176
177    #[test]
178    fn test_field_with_index() {
179        let path = JsonPath::root().push_field("users").push_index(0);
180        assert_eq!(path.to_string(), "users[0]");
181    }
182
183    #[test]
184    fn test_complex_path() {
185        let path = JsonPath::root()
186            .push_field("users")
187            .push_index(0)
188            .push_field("email");
189        assert_eq!(path.to_string(), "users[0].email");
190    }
191
192    #[test]
193    fn test_deeply_nested() {
194        let path = JsonPath::root()
195            .push_field("body")
196            .push_field("data")
197            .push_index(42)
198            .push_field("items")
199            .push_index(0)
200            .push_field("name");
201        assert_eq!(path.to_string(), "body.data[42].items[0].name");
202    }
203
204    #[test]
205    fn test_path_immutability() {
206        let base = JsonPath::root().push_field("users");
207        let path_a = base.push_index(0);
208        let path_b = base.push_index(1);
209
210        assert_eq!(base.to_string(), "users");
211        assert_eq!(path_a.to_string(), "users[0]");
212        assert_eq!(path_b.to_string(), "users[1]");
213    }
214
215    #[test]
216    fn test_parent_path() {
217        let path = JsonPath::root()
218            .push_field("users")
219            .push_index(0)
220            .push_field("email");
221
222        let parent = path.parent().unwrap();
223        assert_eq!(parent.to_string(), "users[0]");
224
225        let grandparent = parent.parent().unwrap();
226        assert_eq!(grandparent.to_string(), "users");
227
228        let root = grandparent.parent().unwrap();
229        assert!(root.is_root());
230
231        assert!(root.parent().is_none());
232    }
233
234    #[test]
235    fn test_from_constructors() {
236        let field_path = JsonPath::from_field("name");
237        assert_eq!(field_path.to_string(), "name");
238
239        let index_path = JsonPath::from_index(5);
240        assert_eq!(index_path.to_string(), "[5]");
241    }
242
243    #[test]
244    fn test_last_segment() {
245        let path = JsonPath::root().push_field("users").push_index(0);
246        assert_eq!(path.last(), Some(&PathSegment::Index(0)));
247
248        let root = JsonPath::root();
249        assert_eq!(root.last(), None);
250    }
251
252    #[test]
253    fn test_segments_iterator() {
254        let path = JsonPath::root()
255            .push_field("a")
256            .push_index(1)
257            .push_field("b");
258
259        let segments: Vec<_> = path.segments().collect();
260        assert_eq!(segments.len(), 3);
261        assert_eq!(segments[0], &PathSegment::Field("a".to_string()));
262        assert_eq!(segments[1], &PathSegment::Index(1));
263        assert_eq!(segments[2], &PathSegment::Field("b".to_string()));
264    }
265
266    #[test]
267    fn test_equality() {
268        let path1 = JsonPath::root().push_field("a").push_index(0);
269        let path2 = JsonPath::root().push_field("a").push_index(0);
270        let path3 = JsonPath::root().push_field("a").push_index(1);
271
272        assert_eq!(path1, path2);
273        assert_ne!(path1, path3);
274    }
275
276    #[test]
277    fn test_clone() {
278        let path = JsonPath::root().push_field("test");
279        let cloned = path.clone();
280        assert_eq!(path, cloned);
281    }
282}