agent_first_data/document/
traverse.rs1use crate::document::{DocumentError, DocumentResult, Value, keyed::KeyedList, path::parse_path};
4
5pub fn get_path_ref<'a>(
12 root: &'a Value,
13 path: &str,
14 keyed_lists: &[KeyedList<'_>],
15) -> DocumentResult<&'a Value> {
16 if path.is_empty() {
17 return Err(DocumentError::EmptyPath);
18 }
19
20 let segments = parse_path(path)?;
21 let mut current = root;
22 let mut accumulated_prefix = String::new();
23 let mut seg_idx = 0;
24
25 while seg_idx < segments.len() {
26 let current_seg = segments[seg_idx].as_str();
27
28 match current {
29 Value::Object(obj) => {
30 if let Some(next) = obj.get(current_seg) {
32 if !accumulated_prefix.is_empty() {
33 accumulated_prefix.push('.');
34 }
35 accumulated_prefix.push_str(current_seg);
36 current = next;
37 seg_idx += 1;
38 } else {
39 return Err(DocumentError::UnknownSegment {
40 path: path.to_string(),
41 segment: current_seg.to_string(),
42 });
43 }
44 }
45 Value::Array(arr) => {
46 if let Ok(arr_idx) = current_seg.parse::<usize>() {
48 let elem = arr
49 .get(arr_idx)
50 .ok_or_else(|| DocumentError::IndexOutOfBounds {
51 path: accumulated_prefix.clone(),
52 index: arr_idx,
53 len: arr.len(),
54 })?;
55 if !accumulated_prefix.is_empty() {
56 accumulated_prefix.push('.');
57 }
58 accumulated_prefix.push_str(current_seg);
59 current = elem;
60 seg_idx += 1;
61 } else {
62 let registration = keyed_lists
63 .iter()
64 .find(|kl| keyed_prefix_matches(kl, &accumulated_prefix))
65 .ok_or_else(|| DocumentError::UnregisteredArray {
66 path: accumulated_prefix.clone(),
67 })?;
68
69 let slug = current_seg;
70 let elem = arr
71 .iter()
72 .find(|e| {
73 if let Some(obj) = e.as_object() {
74 if let Some(Value::String(s)) = obj.get(registration.slug_field) {
75 s == slug
76 } else {
77 false
78 }
79 } else {
80 false
81 }
82 })
83 .ok_or_else(|| DocumentError::SlugNotFound {
84 prefix: accumulated_prefix.clone(),
85 slug: slug.to_string(),
86 })?;
87
88 current = elem;
89 accumulated_prefix.push('.');
90 accumulated_prefix.push_str(slug);
91 seg_idx += 1;
92 }
93 }
94 _ => {
95 return Err(DocumentError::NotTraversable {
96 path: path.to_string(),
97 got: current.kind_name().to_string(),
98 });
99 }
100 }
101 }
102
103 Ok(current)
104}
105
106pub fn get_path(root: &Value, path: &str, keyed_lists: &[KeyedList<'_>]) -> DocumentResult<Value> {
108 Ok(get_path_ref(root, path, keyed_lists)?.clone())
109}
110
111pub fn set_path(
117 root: &mut Value,
118 path: &str,
119 value: &Value,
120 keyed_lists: &[KeyedList<'_>],
121) -> DocumentResult<()> {
122 if path.is_empty() {
123 return Err(DocumentError::EmptyPath);
124 }
125
126 let segments = parse_path(path)?;
127 set_path_recursive(root, &segments, 0, &mut String::new(), keyed_lists, value)
128}
129
130fn set_path_recursive(
131 current: &mut Value,
132 segments: &[String],
133 idx: usize,
134 accumulated_prefix: &mut String,
135 keyed_lists: &[KeyedList<'_>],
136 value: &Value,
137) -> DocumentResult<()> {
138 if idx >= segments.len() {
139 return Err(DocumentError::EmptyPath);
140 }
141
142 let current_seg = segments[idx].as_str();
143 let is_last = idx == segments.len() - 1;
144
145 match current {
146 Value::Object(obj) => {
147 let key_to_use = current_seg.to_string();
149
150 let segments_to_consume = 1;
151
152 if is_last {
153 obj.insert(key_to_use, value.clone());
155 Ok(())
156 } else {
157 let next_idx = idx + segments_to_consume;
159 if next_idx >= segments.len() {
160 return Err(DocumentError::EmptyPath);
161 }
162
163 if !accumulated_prefix.is_empty() {
164 accumulated_prefix.push('.');
165 }
166 accumulated_prefix.push_str(&key_to_use);
167
168 use std::collections::btree_map::Entry;
170 match obj.entry(key_to_use) {
171 Entry::Occupied(mut ent) => set_path_recursive(
172 ent.get_mut(),
173 segments,
174 next_idx,
175 accumulated_prefix,
176 keyed_lists,
177 value,
178 ),
179 Entry::Vacant(ent) => {
180 let mut new_obj = Value::Object(Default::default());
181 let result = set_path_recursive(
182 &mut new_obj,
183 segments,
184 next_idx,
185 accumulated_prefix,
186 keyed_lists,
187 value,
188 );
189 if result.is_ok() {
190 ent.insert(new_obj);
191 }
192 result
193 }
194 }
195 }
196 }
197 Value::Array(arr) => {
198 if let Ok(arr_idx) = current_seg.parse::<usize>() {
200 if arr_idx >= arr.len() {
201 return Err(DocumentError::IndexOutOfBounds {
202 path: accumulated_prefix.clone(),
203 index: arr_idx,
204 len: arr.len(),
205 });
206 }
207 if is_last {
208 arr[arr_idx] = value.clone();
209 Ok(())
210 } else {
211 accumulated_prefix.push('.');
212 accumulated_prefix.push_str(current_seg);
213 set_path_recursive(
214 &mut arr[arr_idx],
215 segments,
216 idx + 1,
217 accumulated_prefix,
218 keyed_lists,
219 value,
220 )
221 }
222 } else {
223 let registration = keyed_lists
224 .iter()
225 .find(|kl| keyed_prefix_matches(kl, accumulated_prefix))
226 .ok_or_else(|| DocumentError::UnregisteredArray {
227 path: accumulated_prefix.clone(),
228 })?;
229
230 let slug = current_seg;
231 let elem_idx = arr
232 .iter()
233 .position(|e| {
234 if let Some(elem_obj) = e.as_object() {
235 if let Some(Value::String(s)) = elem_obj.get(registration.slug_field) {
236 s == slug
237 } else {
238 false
239 }
240 } else {
241 false
242 }
243 })
244 .ok_or_else(|| DocumentError::SlugNotFound {
245 prefix: accumulated_prefix.clone(),
246 slug: slug.to_string(),
247 })?;
248
249 if is_last {
250 Err(DocumentError::UnsupportedOperation {
251 format: "keyed list".to_string(),
252 operation: "set".to_string(),
253 detail:
254 "a keyed-list slug resolves to an element; set a child field instead"
255 .to_string(),
256 })
257 } else {
258 accumulated_prefix.push('.');
259 accumulated_prefix.push_str(slug);
260 set_path_recursive(
261 &mut arr[elem_idx],
262 segments,
263 idx + 1,
264 accumulated_prefix,
265 keyed_lists,
266 value,
267 )
268 }
269 }
270 }
271 _ => Err(DocumentError::NotTraversable {
272 path: accumulated_prefix.clone(),
273 got: current.kind_name().to_string(),
274 }),
275 }
276}
277
278fn keyed_prefix_matches(registration: &KeyedList<'_>, semantic_prefix: &str) -> bool {
279 registration.prefix == semantic_prefix
280 || crate::document::parse_path(registration.prefix)
281 .ok()
282 .is_some_and(|segments| segments.join(".") == semantic_prefix)
283}
284
285pub fn unset_path(root: &mut Value, path: &str) -> DocumentResult<()> {
290 if path.is_empty() {
291 return Err(DocumentError::EmptyPath);
292 }
293 let segments = parse_path(path)?;
294 unset_path_recursive(root, &segments, 0, &mut String::new())
295}
296
297fn unset_path_recursive(
298 current: &mut Value,
299 segments: &[String],
300 idx: usize,
301 accumulated_prefix: &mut String,
302) -> DocumentResult<()> {
303 if idx >= segments.len() {
304 return Err(DocumentError::EmptyPath);
305 }
306 let current_seg = segments[idx].as_str();
307 let is_last = idx == segments.len() - 1;
308
309 match current {
310 Value::Object(obj) => {
311 let key_to_use = current_seg.to_string();
312 let segments_to_consume = 1;
313
314 if is_last {
315 if obj.remove(&key_to_use).is_none() {
316 return Err(DocumentError::PathNotFound { path: key_to_use });
317 }
318 Ok(())
319 } else {
320 let next_idx = idx + segments_to_consume;
321 if !accumulated_prefix.is_empty() {
322 accumulated_prefix.push('.');
323 }
324 accumulated_prefix.push_str(&key_to_use);
325 if let Some(next) = obj.get_mut(&key_to_use) {
326 unset_path_recursive(next, segments, next_idx, accumulated_prefix)
327 } else {
328 Err(DocumentError::PathNotFound {
329 path: accumulated_prefix.clone(),
330 })
331 }
332 }
333 }
334 Value::Array(arr) => {
335 if let Ok(arr_idx) = current_seg.parse::<usize>() {
336 if arr_idx >= arr.len() {
337 return Err(DocumentError::IndexOutOfBounds {
338 path: accumulated_prefix.clone(),
339 index: arr_idx,
340 len: arr.len(),
341 });
342 }
343 if is_last {
344 arr.remove(arr_idx);
345 Ok(())
346 } else {
347 accumulated_prefix.push('.');
348 accumulated_prefix.push_str(current_seg);
349 unset_path_recursive(&mut arr[arr_idx], segments, idx + 1, accumulated_prefix)
350 }
351 } else {
352 Err(DocumentError::UnregisteredArray {
353 path: accumulated_prefix.clone(),
354 })
355 }
356 }
357 _ => Err(DocumentError::NotTraversable {
358 path: accumulated_prefix.clone(),
359 got: current.kind_name().to_string(),
360 }),
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 #![allow(
367 clippy::unwrap_used,
368 clippy::panic,
369 clippy::expect_used,
370 clippy::bool_assert_comparison
371 )]
372 use super::*;
373
374 fn make_test_object() -> Value {
375 let mut root = Value::Object(Default::default());
376 let mut imap = Value::Object(Default::default());
377 imap.as_object_mut().unwrap().insert(
378 "host".to_string(),
379 Value::String("mail.example.com".to_string()),
380 );
381 imap.as_object_mut()
382 .unwrap()
383 .insert("port".to_string(), Value::Integer(993));
384
385 root.as_object_mut()
386 .unwrap()
387 .insert("imap".to_string(), imap);
388
389 root
390 }
391
392 #[test]
393 fn test_get_path_simple() {
394 let root = make_test_object();
395 let result = get_path(&root, "imap.host", &[]).unwrap();
396 assert_eq!(result.as_str().unwrap(), "mail.example.com");
397 }
398
399 #[test]
400 fn test_get_path_integer() {
401 let root = make_test_object();
402 let result = get_path(&root, "imap.port", &[]).unwrap();
403 assert_eq!(result.as_integer().unwrap(), 993);
404 }
405
406 #[test]
407 fn test_set_path_new_key() {
408 let mut root = make_test_object();
409 set_path(&mut root, "imap.tls", &Value::Bool(true), &[]).unwrap();
410
411 let result = get_path(&root, "imap.tls", &[]).unwrap();
412 assert_eq!(result.as_bool().unwrap(), true);
413 }
414
415 #[test]
416 fn test_set_path_overwrite() {
417 let mut root = make_test_object();
418 set_path(&mut root, "imap.port", &Value::Integer(587), &[]).unwrap();
419
420 let result = get_path(&root, "imap.port", &[]).unwrap();
421 assert_eq!(result.as_integer().unwrap(), 587);
422 }
423
424 #[test]
425 fn test_set_path_array_value() {
426 let mut root = Value::Object(Default::default());
427 let value = Value::Array(vec![
428 Value::String("dev".to_string()),
429 Value::String("staging".to_string()),
430 ]);
431 set_path(&mut root, "tags", &value, &[]).unwrap();
432
433 let result = get_path(&root, "tags", &[]).unwrap();
434 let arr = result.as_array().unwrap();
435 assert_eq!(arr.len(), 2);
436 }
437
438 fn make_steps_object() -> Value {
439 let mut root = Value::Object(Default::default());
441 let mut s0 = Value::Object(Default::default());
442 s0.as_object_mut()
443 .unwrap()
444 .insert("name".to_string(), Value::String("a".to_string()));
445 s0.as_object_mut()
446 .unwrap()
447 .insert("port".to_string(), Value::Integer(1));
448 let mut s1 = Value::Object(Default::default());
449 s1.as_object_mut()
450 .unwrap()
451 .insert("name".to_string(), Value::String("b".to_string()));
452 s1.as_object_mut()
453 .unwrap()
454 .insert("port".to_string(), Value::Integer(2));
455 root.as_object_mut()
456 .unwrap()
457 .insert("steps".to_string(), Value::Array(vec![s0, s1]));
458 root
459 }
460
461 #[test]
462 fn test_get_path_numeric_index() {
463 let root = make_steps_object();
464 let name = get_path(&root, "steps.0.name", &[]).unwrap();
465 assert_eq!(name.as_str().unwrap(), "a");
466 let port = get_path(&root, "steps.1.port", &[]).unwrap();
467 assert_eq!(port.as_integer().unwrap(), 2);
468 }
469
470 #[test]
471 fn test_set_path_numeric_index() {
472 let mut root = make_steps_object();
473 set_path(&mut root, "steps.0.port", &Value::Integer(99), &[]).unwrap();
474 let result = get_path(&root, "steps.0.port", &[]).unwrap();
475 assert_eq!(result.as_integer().unwrap(), 99);
476 let other = get_path(&root, "steps.1.port", &[]).unwrap();
478 assert_eq!(other.as_integer().unwrap(), 2);
479 }
480
481 #[test]
482 fn test_get_path_index_out_of_bounds() {
483 let root = make_steps_object();
484 let err = get_path(&root, "steps.5.name", &[]).unwrap_err();
485 assert!(matches!(
486 err,
487 DocumentError::IndexOutOfBounds {
488 index: 5,
489 len: 2,
490 ..
491 }
492 ));
493 }
494
495 #[test]
496 fn test_remove_path_numeric_index() {
497 let mut root = make_steps_object();
498 unset_path(&mut root, "steps.0").unwrap();
499 let arr = get_path(&root, "steps", &[]).unwrap();
500 let arr = arr.as_array().unwrap();
501 assert_eq!(arr.len(), 1);
502 assert_eq!(arr[0].get("name").unwrap().as_str().unwrap(), "b");
503 }
504}