pjson_rs_domain/value_objects/
json_path.rs1use crate::{DomainError, DomainResult};
7use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct JsonPath(String);
15
16impl JsonPath {
17 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 pub fn root() -> Self {
26 Self("$".to_string())
28 }
29
30 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 pub fn append_index(&self, index: usize) -> Self {
53 let new_path = format!("{}[{index}]", self.0);
54 Self(new_path)
56 }
57
58 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62
63 pub fn parent(&self) -> Option<Self> {
65 if self.0 == "$" {
66 return None;
67 }
68
69 if let Some(pos) = self.0.rfind('.') {
71 if pos > 1 {
72 return Some(Self(self.0[..pos].to_string()));
74 } else {
75 return Some(Self::root());
76 }
77 }
78
79 if let Some(pos) = self.0.rfind('[')
81 && pos > 1
82 {
83 return Some(Self(self.0[..pos].to_string()));
84 }
85
86 Some(Self::root())
88 }
89
90 pub fn last_segment(&self) -> Option<PathSegment> {
92 if self.0 == "$" {
93 return Some(PathSegment::Root);
94 }
95
96 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 if let Some(pos) = self.0.rfind('.') {
109 let key = &self.0[pos + 1..];
110 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 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 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 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 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(()); }
179
180 let mut chars = path.chars().skip(1).peekable();
182
183 while let Some(ch) = chars.next() {
184 match ch {
185 '.' => {
186 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
247#[non_exhaustive]
248pub enum PathSegment {
249 Root,
251 Key(String),
253 Index(usize),
255}
256
257impl fmt::Display for JsonPath {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 write!(f, "{}", self.0)
260 }
261}
262
263impl fmt::Display for PathSegment {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 match self {
266 PathSegment::Root => write!(f, "$"),
267 PathSegment::Key(key) => write!(f, ".{key}"),
268 PathSegment::Index(index) => write!(f, "[{index}]"),
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_valid_paths() {
279 assert!(JsonPath::new("$").is_ok());
280 assert!(JsonPath::new("$.key").is_ok());
281 assert!(JsonPath::new("$.key.nested").is_ok());
282 assert!(JsonPath::new("$.key[0]").is_ok());
283 assert!(JsonPath::new("$.array[123].field").is_ok());
284 }
285
286 #[test]
287 fn test_invalid_paths() {
288 assert!(JsonPath::new("").is_err());
289 assert!(JsonPath::new("key").is_err());
290 assert!(JsonPath::new("$.").is_err());
291 assert!(JsonPath::new("$.key.").is_err());
292 assert!(JsonPath::new("$.key[]").is_err());
293 assert!(JsonPath::new("$.key[abc]").is_err());
294 assert!(JsonPath::new("$.key with spaces").is_err());
295 }
296
297 #[test]
298 fn test_path_operations() {
299 let root = JsonPath::root();
300 let path = root
301 .append_key("users")
302 .unwrap()
303 .append_index(0)
304 .append_key("name")
305 .unwrap();
306
307 assert_eq!(path.as_str(), "$.users[0].name");
308 assert_eq!(path.depth(), 3);
309 }
310
311 #[test]
312 fn test_parent_path() {
313 let path = JsonPath::new("$.users[0].name").unwrap();
314 let parent = path.parent().unwrap();
315 assert_eq!(parent.as_str(), "$.users[0]");
316
317 let root = JsonPath::root();
318 assert!(root.parent().is_none());
319 }
320
321 #[test]
322 fn test_last_segment() {
323 let path1 = JsonPath::new("$.users").unwrap();
324 assert_eq!(
325 path1.last_segment(),
326 Some(PathSegment::Key("users".to_string()))
327 );
328
329 let path2 = JsonPath::new("$.array[42]").unwrap();
330 assert_eq!(path2.last_segment(), Some(PathSegment::Index(42)));
331
332 let root = JsonPath::root();
333 assert_eq!(root.last_segment(), Some(PathSegment::Root));
334 }
335
336 #[test]
337 fn test_prefix() {
338 let parent = JsonPath::new("$.users").unwrap();
339 let child = JsonPath::new("$.users.name").unwrap();
340
341 assert!(parent.is_prefix_of(&child));
342 assert!(!child.is_prefix_of(&parent));
343 }
344}