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::domain::{DomainError, DomainResult};
9use std::fmt;
10use serde::{Deserialize, Serialize};
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            if pos > 1 {
88                return Some(Self(self.0[..pos].to_string()));
89            }
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            if let Some(end) = self.0.rfind(']') {
105                if end > start {
106                    let index_str = &self.0[start + 1..end];
107                    if let Ok(index) = index_str.parse::<usize>() {
108                        return Some(PathSegment::Index(index));
109                    }
110                }
111            }
112        }
113
114        // Check for key segment
115        if let Some(pos) = self.0.rfind('.') {
116            let key = &self.0[pos + 1..];
117            // Remove array index if present
118            let key = if let Some(bracket) = key.find('[') {
119                &key[..bracket]
120            } else {
121                key
122            };
123
124            if !key.is_empty() {
125                return Some(PathSegment::Key(key.to_string()));
126            }
127        }
128
129        None
130    }
131
132    /// Get the depth of the path (number of segments)
133    pub fn depth(&self) -> usize {
134        if self.0 == "$" {
135            return 0;
136        }
137
138        let mut depth = 0;
139        let mut chars = self.0.chars().peekable();
140
141        while let Some(ch) = chars.next() {
142            match ch {
143                '.' => depth += 1,
144                '[' => {
145                    // Skip to end of array index
146                    while let Some(ch) = chars.next() {
147                        if ch == ']' {
148                            break;
149                        }
150                    }
151                    depth += 1;
152                }
153                _ => {}
154            }
155        }
156
157        depth
158    }
159
160    /// Check if this path is a prefix of another path
161    pub fn is_prefix_of(&self, other: &JsonPath) -> bool {
162        if self.0.len() >= other.0.len() {
163            return false;
164        }
165
166        other.0.starts_with(&self.0)
167            && (other.0.chars().nth(self.0.len()) == Some('.')
168                || other.0.chars().nth(self.0.len()) == Some('['))
169    }
170
171    /// Validate JSON path format
172    fn validate(path: &str) -> DomainResult<()> {
173        if path.is_empty() {
174            return Err(DomainError::InvalidPath("Path cannot be empty".to_string()));
175        }
176
177        if !path.starts_with('$') {
178            return Err(DomainError::InvalidPath(
179                "Path must start with '$'".to_string(),
180            ));
181        }
182
183        if path.len() == 1 {
184            return Ok(()); // Root path is valid
185        }
186
187        // Validate format: $[.key|[index]]*
188        let mut chars = path.chars().skip(1).peekable();
189
190        while let Some(ch) = chars.next() {
191            match ch {
192                '.' => {
193                    // Must be followed by valid key
194                    let mut key = String::new();
195
196                    while let Some(&next_ch) = chars.peek() {
197                        if next_ch == '.' || next_ch == '[' {
198                            break;
199                        }
200                        key.push(chars.next()
201                            .ok_or_else(|| DomainError::InvalidPath("Incomplete key segment".to_string()))?);
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                    while let Some(ch) = chars.next() {
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
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub enum PathSegment {
254    Root,
255    Key(String),
256    Index(usize),
257}
258
259impl fmt::Display for JsonPath {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        write!(f, "{}", self.0)
262    }
263}
264
265impl fmt::Display for PathSegment {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        match self {
268            PathSegment::Root => write!(f, "$"),
269            PathSegment::Key(key) => write!(f, ".{key}"),
270            PathSegment::Index(index) => write!(f, "[{index}]"),
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    #[test]
280    fn test_valid_paths() {
281        assert!(JsonPath::new("$").is_ok());
282        assert!(JsonPath::new("$.key").is_ok());
283        assert!(JsonPath::new("$.key.nested").is_ok());
284        assert!(JsonPath::new("$.key[0]").is_ok());
285        assert!(JsonPath::new("$.array[123].field").is_ok());
286    }
287
288    #[test]
289    fn test_invalid_paths() {
290        assert!(JsonPath::new("").is_err());
291        assert!(JsonPath::new("key").is_err());
292        assert!(JsonPath::new("$.").is_err());
293        assert!(JsonPath::new("$.key.").is_err());
294        assert!(JsonPath::new("$.key[]").is_err());
295        assert!(JsonPath::new("$.key[abc]").is_err());
296        assert!(JsonPath::new("$.key with spaces").is_err());
297    }
298
299    #[test]
300    fn test_path_operations() {
301        let root = JsonPath::root();
302        let path = root
303            .append_key("users")
304            .unwrap()
305            .append_index(0)
306            .append_key("name")
307            .unwrap();
308
309        assert_eq!(path.as_str(), "$.users[0].name");
310        assert_eq!(path.depth(), 3);
311    }
312
313    #[test]
314    fn test_parent_path() {
315        let path = JsonPath::new("$.users[0].name").unwrap();
316        let parent = path.parent().unwrap();
317        assert_eq!(parent.as_str(), "$.users[0]");
318
319        let root = JsonPath::root();
320        assert!(root.parent().is_none());
321    }
322
323    #[test]
324    fn test_last_segment() {
325        let path1 = JsonPath::new("$.users").unwrap();
326        assert_eq!(
327            path1.last_segment(),
328            Some(PathSegment::Key("users".to_string()))
329        );
330
331        let path2 = JsonPath::new("$.array[42]").unwrap();
332        assert_eq!(path2.last_segment(), Some(PathSegment::Index(42)));
333
334        let root = JsonPath::root();
335        assert_eq!(root.last_segment(), Some(PathSegment::Root));
336    }
337
338    #[test]
339    fn test_prefix() {
340        let parent = JsonPath::new("$.users").unwrap();
341        let child = JsonPath::new("$.users.name").unwrap();
342
343        assert!(parent.is_prefix_of(&child));
344        assert!(!child.is_prefix_of(&parent));
345    }
346}