Skip to main content

pjson_rs_domain/value_objects/
json_path.rs

1//! JSON Path Value Object with validation
2//!
3//! Pure domain object for JSON path addressing.
4//! Serialization is handled in the application layer via DTOs.
5
6use crate::{DomainError, DomainResult};
7use std::fmt;
8
9/// Type-safe JSON Path for addressing nodes in JSON structures
10///
11/// This is a pure domain object with no serialization concerns.
12/// For serialization, use `JsonPathDto` from the application layer.
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct JsonPath(String);
15
16impl JsonPath {
17    /// Create new JSON path with validation
18    pub fn new(path: impl Into<String>) -> DomainResult<Self> {
19        let path = path.into();
20        Self::validate(&path)?;
21        Ok(Self(path))
22    }
23
24    /// Create root path
25    pub fn root() -> Self {
26        // Safety: Root path is always valid
27        Self("$".to_string())
28    }
29
30    /// Append a key to the path
31    pub fn append_key(&self, key: &str) -> DomainResult<Self> {
32        if key.is_empty() {
33            return Err(DomainError::InvalidPath("Key cannot be empty".to_string()));
34        }
35
36        if key.contains('.') || key.contains('[') || key.contains(']') {
37            return Err(DomainError::InvalidPath(format!(
38                "Key '{key}' contains invalid characters"
39            )));
40        }
41
42        let new_path = if self.0 == "$" {
43            format!("$.{key}")
44        } else {
45            format!("{}.{key}", self.0)
46        };
47
48        Ok(Self(new_path))
49    }
50
51    /// Append an array index to the path
52    pub fn append_index(&self, index: usize) -> Self {
53        let new_path = format!("{}[{index}]", self.0);
54        // Array index paths are always valid if base path is valid
55        Self(new_path)
56    }
57
58    /// Get the path as a string
59    pub fn as_str(&self) -> &str {
60        &self.0
61    }
62
63    /// Get the parent path (if any)
64    pub fn parent(&self) -> Option<Self> {
65        if self.0 == "$" {
66            return None;
67        }
68
69        // Find last separator
70        if let Some(pos) = self.0.rfind('.') {
71            if pos > 1 {
72                // Ensure we don't return empty path
73                return Some(Self(self.0[..pos].to_string()));
74            } else {
75                return Some(Self::root());
76            }
77        }
78
79        // Handle array index case: $.key[0] -> $.key
80        if let Some(pos) = self.0.rfind('[')
81            && pos > 1
82        {
83            return Some(Self(self.0[..pos].to_string()));
84        }
85
86        // If no separator found and not root, parent is root
87        Some(Self::root())
88    }
89
90    /// Get the last segment of the path
91    pub fn last_segment(&self) -> Option<PathSegment> {
92        if self.0 == "$" {
93            return Some(PathSegment::Root);
94        }
95
96        // Check for array index: [123]
97        if let Some(start) = self.0.rfind('[')
98            && let Some(end) = self.0.rfind(']')
99            && end > start
100        {
101            let index_str = &self.0[start + 1..end];
102            if let Ok(index) = index_str.parse::<usize>() {
103                return Some(PathSegment::Index(index));
104            }
105        }
106
107        // Check for key segment
108        if let Some(pos) = self.0.rfind('.') {
109            let key = &self.0[pos + 1..];
110            // Remove array index if present
111            let key = if let Some(bracket) = key.find('[') {
112                &key[..bracket]
113            } else {
114                key
115            };
116
117            if !key.is_empty() {
118                return Some(PathSegment::Key(key.to_string()));
119            }
120        }
121
122        None
123    }
124
125    /// Get the depth of the path (number of segments)
126    pub fn depth(&self) -> usize {
127        if self.0 == "$" {
128            return 0;
129        }
130
131        let mut depth = 0;
132        let mut chars = self.0.chars().peekable();
133
134        while let Some(ch) = chars.next() {
135            match ch {
136                '.' => depth += 1,
137                '[' => {
138                    // Skip to end of array index
139                    for ch in chars.by_ref() {
140                        if ch == ']' {
141                            break;
142                        }
143                    }
144                    depth += 1;
145                }
146                _ => {}
147            }
148        }
149
150        depth
151    }
152
153    /// Check if this path is a prefix of another path
154    pub fn is_prefix_of(&self, other: &JsonPath) -> bool {
155        if self.0.len() >= other.0.len() {
156            return false;
157        }
158
159        other.0.starts_with(&self.0)
160            && (other.0.chars().nth(self.0.len()) == Some('.')
161                || other.0.chars().nth(self.0.len()) == Some('['))
162    }
163
164    /// Validate JSON path format
165    fn validate(path: &str) -> DomainResult<()> {
166        if path.is_empty() {
167            return Err(DomainError::InvalidPath("Path cannot be empty".to_string()));
168        }
169
170        if !path.starts_with('$') {
171            return Err(DomainError::InvalidPath(
172                "Path must start with '$'".to_string(),
173            ));
174        }
175
176        if path.len() == 1 {
177            return Ok(()); // Root path is valid
178        }
179
180        // Validate format: $[.key|[index]]*
181        let mut chars = path.chars().skip(1).peekable();
182
183        while let Some(ch) = chars.next() {
184            match ch {
185                '.' => {
186                    // Must be followed by valid key
187                    let mut key = String::new();
188
189                    while let Some(&next_ch) = chars.peek() {
190                        if next_ch == '.' || next_ch == '[' {
191                            break;
192                        }
193                        key.push(chars.next().ok_or_else(|| {
194                            DomainError::InvalidPath("Incomplete key segment".to_string())
195                        })?);
196                    }
197
198                    if key.is_empty() {
199                        return Err(DomainError::InvalidPath("Empty key segment".to_string()));
200                    }
201
202                    // Validate key characters
203                    if !key
204                        .chars()
205                        .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
206                    {
207                        return Err(DomainError::InvalidPath(format!(
208                            "Invalid characters in key '{key}'"
209                        )));
210                    }
211                }
212                '[' => {
213                    // Must contain valid array index
214                    let mut index_str = String::new();
215
216                    for ch in chars.by_ref() {
217                        if ch == ']' {
218                            break;
219                        }
220                        index_str.push(ch);
221                    }
222
223                    if index_str.is_empty() {
224                        return Err(DomainError::InvalidPath("Empty array index".to_string()));
225                    }
226
227                    if index_str.parse::<usize>().is_err() {
228                        return Err(DomainError::InvalidPath(format!(
229                            "Invalid array index '{index_str}'"
230                        )));
231                    }
232                }
233                _ => {
234                    return Err(DomainError::InvalidPath(format!(
235                        "Unexpected character '{ch}' in path"
236                    )));
237                }
238            }
239        }
240
241        Ok(())
242    }
243}
244
245/// Path segment types for traversing JSON structures
246#[derive(Debug, Clone, PartialEq, Eq)]
247pub enum PathSegment {
248    /// Root of the JSON document
249    Root,
250    /// Object key access
251    Key(String),
252    /// Array index access
253    Index(usize),
254}
255
256impl fmt::Display for JsonPath {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        write!(f, "{}", self.0)
259    }
260}
261
262impl fmt::Display for PathSegment {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        match self {
265            PathSegment::Root => write!(f, "$"),
266            PathSegment::Key(key) => write!(f, ".{key}"),
267            PathSegment::Index(index) => write!(f, "[{index}]"),
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_valid_paths() {
278        assert!(JsonPath::new("$").is_ok());
279        assert!(JsonPath::new("$.key").is_ok());
280        assert!(JsonPath::new("$.key.nested").is_ok());
281        assert!(JsonPath::new("$.key[0]").is_ok());
282        assert!(JsonPath::new("$.array[123].field").is_ok());
283    }
284
285    #[test]
286    fn test_invalid_paths() {
287        assert!(JsonPath::new("").is_err());
288        assert!(JsonPath::new("key").is_err());
289        assert!(JsonPath::new("$.").is_err());
290        assert!(JsonPath::new("$.key.").is_err());
291        assert!(JsonPath::new("$.key[]").is_err());
292        assert!(JsonPath::new("$.key[abc]").is_err());
293        assert!(JsonPath::new("$.key with spaces").is_err());
294    }
295
296    #[test]
297    fn test_path_operations() {
298        let root = JsonPath::root();
299        let path = root
300            .append_key("users")
301            .unwrap()
302            .append_index(0)
303            .append_key("name")
304            .unwrap();
305
306        assert_eq!(path.as_str(), "$.users[0].name");
307        assert_eq!(path.depth(), 3);
308    }
309
310    #[test]
311    fn test_parent_path() {
312        let path = JsonPath::new("$.users[0].name").unwrap();
313        let parent = path.parent().unwrap();
314        assert_eq!(parent.as_str(), "$.users[0]");
315
316        let root = JsonPath::root();
317        assert!(root.parent().is_none());
318    }
319
320    #[test]
321    fn test_last_segment() {
322        let path1 = JsonPath::new("$.users").unwrap();
323        assert_eq!(
324            path1.last_segment(),
325            Some(PathSegment::Key("users".to_string()))
326        );
327
328        let path2 = JsonPath::new("$.array[42]").unwrap();
329        assert_eq!(path2.last_segment(), Some(PathSegment::Index(42)));
330
331        let root = JsonPath::root();
332        assert_eq!(root.last_segment(), Some(PathSegment::Root));
333    }
334
335    #[test]
336    fn test_prefix() {
337        let parent = JsonPath::new("$.users").unwrap();
338        let child = JsonPath::new("$.users.name").unwrap();
339
340        assert!(parent.is_prefix_of(&child));
341        assert!(!child.is_prefix_of(&parent));
342    }
343}