pjson_rs/domain/value_objects/
json_path.rs1use crate::domain::{DomainError, DomainResult};
9use std::fmt;
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct JsonPath(String);
21
22impl JsonPath {
23 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 pub fn root() -> Self {
32 Self("$".to_string())
34 }
35
36 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 pub fn append_index(&self, index: usize) -> Self {
59 let new_path = format!("{}[{index}]", self.0);
60 Self(new_path)
62 }
63
64 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68
69 pub fn parent(&self) -> Option<Self> {
71 if self.0 == "$" {
72 return None;
73 }
74
75 if let Some(pos) = self.0.rfind('.') {
77 if pos > 1 {
78 return Some(Self(self.0[..pos].to_string()));
80 } else {
81 return Some(Self::root());
82 }
83 }
84
85 if let Some(pos) = self.0.rfind('[') {
87 if pos > 1 {
88 return Some(Self(self.0[..pos].to_string()));
89 }
90 }
91
92 Some(Self::root())
94 }
95
96 pub fn last_segment(&self) -> Option<PathSegment> {
98 if self.0 == "$" {
99 return Some(PathSegment::Root);
100 }
101
102 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 if let Some(pos) = self.0.rfind('.') {
116 let key = &self.0[pos + 1..];
117 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 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 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 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 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(()); }
186
187 let mut chars = path.chars().skip(1).peekable();
189
190 while let Some(ch) = chars.next() {
191 match ch {
192 '.' => {
193 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 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 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#[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}