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