1use crate::document::{
4 DocumentError, DocumentResult, Value,
5 keyed::{Addressing, KeyedList},
6 path::{join_path, parse_path},
7};
8
9pub(crate) fn append_position_refused(prefix: &[String], last: bool) -> DocumentError {
24 let detail = if last {
25 format!(
26 "`-` names the append position after the last element of `{}`, which is only \
27 valid as the last segment of a `set` path; address an existing element by index",
28 join_path(prefix)
29 )
30 } else {
31 format!(
32 "`-` names the append position of `{}` and can only be the last segment of a \
33 path; address an existing element by index",
34 join_path(prefix)
35 )
36 };
37 DocumentError::PathSyntax { detail }
38}
39
40fn resolve_named_element(
41 array: &[Value],
42 segment: &str,
43 prefix: &[String],
44 last: bool,
45 addressing: Addressing<'_>,
46) -> DocumentResult<usize> {
47 if segment == "-" {
48 return Err(append_position_refused(prefix, last));
49 }
50 if let Some(registration) = addressing
53 .keyed_lists
54 .iter()
55 .find(|list| keyed_prefix_matches(list, prefix))
56 {
57 let hits = array
58 .iter()
59 .enumerate()
60 .filter(|(_, element)| {
61 element.get(registration.slug_field).and_then(Value::as_str) == Some(segment)
62 })
63 .map(|(index, _)| index)
64 .collect();
65 return resolve_unique_hit(hits, segment, &join_path(prefix));
66 }
67
68 let Some(rule) = addressing.array_rule else {
69 return Err(DocumentError::UnregisteredArray {
70 path: join_path(prefix),
71 });
72 };
73
74 let hits: Vec<usize> = array
75 .iter()
76 .enumerate()
77 .filter(|(_, element)| rule.matches(element, segment))
78 .map(|(index, _)| index)
79 .collect();
80 resolve_unique_hit(hits, segment, &join_path(prefix))
81}
82
83fn resolve_unique_hit(hits: Vec<usize>, segment: &str, prefix: &str) -> DocumentResult<usize> {
84 match hits.as_slice() {
85 [] => Err(DocumentError::SlugNotFound {
86 prefix: prefix.to_string(),
87 slug: segment.to_string(),
88 }),
89 [only] => Ok(*only),
90 several => Err(DocumentError::AmbiguousMatch {
91 prefix: prefix.to_string(),
92 segment: segment.to_string(),
93 indices: several.to_vec(),
94 }),
95 }
96}
97
98fn array_index(segment: &str) -> DocumentResult<Option<usize>> {
106 if segment.is_empty() || !segment.bytes().all(|byte| byte.is_ascii_digit()) {
107 return Ok(None);
108 }
109 segment
110 .parse::<usize>()
111 .map(Some)
112 .map_err(|_| DocumentError::PathSyntax {
113 detail: "array index exceeds the platform index range".to_string(),
114 })
115}
116
117pub fn get_path_ref<'a>(
124 root: &'a Value,
125 path: &str,
126 addressing: Addressing<'_>,
127) -> DocumentResult<&'a Value> {
128 if path.is_empty() {
129 return Err(DocumentError::EmptyPath);
130 }
131
132 let segments = parse_path(path)?;
133 let mut current = root;
134 let mut accumulated_prefix: Vec<String> = Vec::new();
135 let mut seg_idx = 0;
136
137 while seg_idx < segments.len() {
138 let current_seg = segments[seg_idx].as_str();
139
140 match current {
141 Value::Object(obj) => {
142 if let Some(next) = obj.get(current_seg) {
144 accumulated_prefix.push(current_seg.to_string());
145 current = next;
146 seg_idx += 1;
147 } else {
148 return Err(DocumentError::UnknownSegment {
149 path: path.to_string(),
150 segment: current_seg.to_string(),
151 });
152 }
153 }
154 Value::Array(arr) => {
155 if let Some(arr_idx) = array_index(current_seg)? {
157 let elem = arr
158 .get(arr_idx)
159 .ok_or_else(|| DocumentError::IndexOutOfBounds {
160 path: join_path(&accumulated_prefix),
161 index: arr_idx,
162 len: arr.len(),
163 })?;
164 accumulated_prefix.push(current_seg.to_string());
165 current = elem;
166 seg_idx += 1;
167 } else {
168 let index = resolve_named_element(
169 arr,
170 current_seg,
171 &accumulated_prefix,
172 seg_idx + 1 == segments.len(),
173 addressing,
174 )?;
175 current = &arr[index];
176 accumulated_prefix.push(current_seg.to_string());
177 seg_idx += 1;
178 }
179 }
180 _ => {
181 return Err(DocumentError::NotTraversable {
182 path: join_path(&accumulated_prefix),
183 got: current.kind_name().to_string(),
184 });
185 }
186 }
187 }
188
189 Ok(current)
190}
191
192pub fn get_path(root: &Value, path: &str, addressing: Addressing<'_>) -> DocumentResult<Value> {
194 Ok(get_path_ref(root, path, addressing)?.clone())
195}
196
197pub fn resolve_path(
219 root: &Value,
220 path: &str,
221 addressing: Addressing<'_>,
222) -> DocumentResult<String> {
223 if path.is_empty() {
224 return Ok(String::new());
225 }
226
227 let segments = parse_path(path)?;
228 let mut current = root;
229 let mut semantic: Vec<String> = Vec::new();
230 let mut canonical: Vec<String> = Vec::with_capacity(segments.len());
231
232 for (idx, segment) in segments.iter().enumerate() {
233 let copy_rest = |canonical: &mut Vec<String>| {
234 canonical.extend(segments[idx..].iter().cloned());
235 };
236 match current {
237 Value::Object(object) => match object.get(segment.as_str()) {
238 Some(next) => {
239 canonical.push(segment.clone());
240 semantic.push(segment.clone());
241 current = next;
242 }
243 None => {
244 copy_rest(&mut canonical);
245 break;
246 }
247 },
248 Value::Array(array) => {
249 if segment == "-" && idx + 1 == segments.len() {
250 canonical.push(segment.clone());
251 break;
252 }
253 let index = match array_index(segment)? {
254 Some(index) => index,
255 None => resolve_named_element(
256 array,
257 segment,
258 &semantic,
259 idx + 1 == segments.len(),
260 addressing,
261 )?,
262 };
263 let Some(next) = array.get(index) else {
264 copy_rest(&mut canonical);
265 break;
266 };
267 canonical.push(index.to_string());
268 semantic.push(segment.clone());
269 current = next;
270 }
271 _ => {
272 copy_rest(&mut canonical);
273 break;
274 }
275 }
276 }
277
278 Ok(join_path(&canonical))
279}
280
281pub fn set_path(
287 root: &mut Value,
288 path: &str,
289 value: &Value,
290 addressing: Addressing<'_>,
291) -> DocumentResult<()> {
292 if path.is_empty() {
293 return Err(DocumentError::EmptyPath);
294 }
295
296 let segments = parse_path(path)?;
297 set_path_recursive(root, &segments, 0, &mut Vec::new(), addressing, value)
298}
299
300fn set_path_recursive(
301 current: &mut Value,
302 segments: &[String],
303 idx: usize,
304 accumulated_prefix: &mut Vec<String>,
305 addressing: Addressing<'_>,
306 value: &Value,
307) -> DocumentResult<()> {
308 if idx >= segments.len() {
309 return Err(DocumentError::EmptyPath);
310 }
311
312 let current_seg = segments[idx].as_str();
313 let is_last = idx == segments.len() - 1;
314
315 match current {
316 Value::Object(obj) => {
317 let key_to_use = current_seg.to_string();
319
320 let segments_to_consume = 1;
321
322 if is_last {
323 obj.insert(key_to_use, value.clone());
325 Ok(())
326 } else {
327 let next_idx = idx + segments_to_consume;
329 if next_idx >= segments.len() {
330 return Err(DocumentError::EmptyPath);
331 }
332
333 accumulated_prefix.push(key_to_use.clone());
334
335 use std::collections::btree_map::Entry;
337 match obj.entry(key_to_use) {
338 Entry::Occupied(mut ent) => set_path_recursive(
339 ent.get_mut(),
340 segments,
341 next_idx,
342 accumulated_prefix,
343 addressing,
344 value,
345 ),
346 Entry::Vacant(ent) => {
347 let mut new_obj = Value::Object(Default::default());
348 let result = set_path_recursive(
349 &mut new_obj,
350 segments,
351 next_idx,
352 accumulated_prefix,
353 addressing,
354 value,
355 );
356 if result.is_ok() {
357 ent.insert(new_obj);
358 }
359 result
360 }
361 }
362 }
363 }
364 Value::Array(arr) => {
365 if current_seg == "-" && is_last {
366 arr.push(value.clone());
367 return Ok(());
368 }
369 if let Some(arr_idx) = array_index(current_seg)? {
371 if arr_idx >= arr.len() {
372 return Err(DocumentError::IndexOutOfBounds {
373 path: join_path(accumulated_prefix),
374 index: arr_idx,
375 len: arr.len(),
376 });
377 }
378 if is_last {
379 arr[arr_idx] = value.clone();
380 Ok(())
381 } else {
382 accumulated_prefix.push(current_seg.to_string());
383 set_path_recursive(
384 &mut arr[arr_idx],
385 segments,
386 idx + 1,
387 accumulated_prefix,
388 addressing,
389 value,
390 )
391 }
392 } else {
393 let elem_idx = resolve_named_element(
394 arr,
395 current_seg,
396 accumulated_prefix,
397 is_last,
398 addressing,
399 )?;
400 if is_last {
401 Err(DocumentError::UnsupportedOperation {
402 format: "keyed list".to_string(),
403 operation: "set".to_string(),
404 detail:
405 "a keyed-list slug resolves to an element; set a child field instead"
406 .to_string(),
407 })
408 } else {
409 accumulated_prefix.push(current_seg.to_string());
410 set_path_recursive(
411 &mut arr[elem_idx],
412 segments,
413 idx + 1,
414 accumulated_prefix,
415 addressing,
416 value,
417 )
418 }
419 }
420 }
421 _ => Err(DocumentError::NotTraversable {
422 path: join_path(accumulated_prefix),
423 got: current.kind_name().to_string(),
424 }),
425 }
426}
427
428pub(crate) fn keyed_prefix_matches(
429 registration: &KeyedList<'_>,
430 semantic_prefix: &[String],
431) -> bool {
432 if registration.prefix.is_empty() {
433 return semantic_prefix.is_empty();
434 }
435 crate::document::parse_path(registration.prefix)
436 .ok()
437 .is_some_and(|segments| segments == semantic_prefix)
438}
439
440pub fn unset_path(root: &mut Value, path: &str) -> DocumentResult<()> {
445 if path.is_empty() {
446 return Err(DocumentError::EmptyPath);
447 }
448 let segments = parse_path(path)?;
449 unset_path_recursive(root, &segments, 0, &mut Vec::new())
450}
451
452fn unset_path_recursive(
453 current: &mut Value,
454 segments: &[String],
455 idx: usize,
456 accumulated_prefix: &mut Vec<String>,
457) -> DocumentResult<()> {
458 if idx >= segments.len() {
459 return Err(DocumentError::EmptyPath);
460 }
461 let current_seg = segments[idx].as_str();
462 let is_last = idx == segments.len() - 1;
463
464 match current {
465 Value::Object(obj) => {
466 let key_to_use = current_seg.to_string();
467 let segments_to_consume = 1;
468
469 if is_last {
470 if obj.remove(&key_to_use).is_none() {
471 return Err(DocumentError::PathNotFound { path: key_to_use });
472 }
473 Ok(())
474 } else {
475 let next_idx = idx + segments_to_consume;
476 accumulated_prefix.push(key_to_use.clone());
477 if let Some(next) = obj.get_mut(&key_to_use) {
478 unset_path_recursive(next, segments, next_idx, accumulated_prefix)
479 } else {
480 Err(DocumentError::PathNotFound {
481 path: join_path(accumulated_prefix),
482 })
483 }
484 }
485 }
486 Value::Array(arr) => {
487 if let Some(arr_idx) = array_index(current_seg)? {
488 if arr_idx >= arr.len() {
489 return Err(DocumentError::IndexOutOfBounds {
490 path: join_path(accumulated_prefix),
491 index: arr_idx,
492 len: arr.len(),
493 });
494 }
495 if is_last {
496 arr.remove(arr_idx);
497 Ok(())
498 } else {
499 accumulated_prefix.push(current_seg.to_string());
500 unset_path_recursive(&mut arr[arr_idx], segments, idx + 1, accumulated_prefix)
501 }
502 } else if current_seg == "-" {
503 Err(append_position_refused(accumulated_prefix, is_last))
504 } else {
505 Err(DocumentError::UnregisteredArray {
506 path: join_path(accumulated_prefix),
507 })
508 }
509 }
510 _ => Err(DocumentError::NotTraversable {
511 path: join_path(accumulated_prefix),
512 got: current.kind_name().to_string(),
513 }),
514 }
515}
516
517#[cfg(test)]
518mod tests {
519 #![allow(
520 clippy::unwrap_used,
521 clippy::panic,
522 clippy::expect_used,
523 clippy::bool_assert_comparison
524 )]
525 use super::*;
526
527 fn make_test_object() -> Value {
528 let mut root = Value::Object(Default::default());
529 let mut imap = Value::Object(Default::default());
530 imap.as_object_mut().unwrap().insert(
531 "host".to_string(),
532 Value::String("mail.example.com".to_string()),
533 );
534 imap.as_object_mut()
535 .unwrap()
536 .insert("port".to_string(), Value::Integer(993));
537
538 root.as_object_mut()
539 .unwrap()
540 .insert("imap".to_string(), imap);
541
542 root
543 }
544
545 #[test]
546 fn test_get_path_simple() {
547 let root = make_test_object();
548 let result = get_path(&root, "imap.host", Addressing::INDEX_ONLY).unwrap();
549 assert_eq!(result.as_str().unwrap(), "mail.example.com");
550 }
551
552 #[test]
553 fn test_get_path_integer() {
554 let root = make_test_object();
555 let result = get_path(&root, "imap.port", Addressing::INDEX_ONLY).unwrap();
556 assert_eq!(result.as_integer().unwrap(), 993);
557 }
558
559 #[test]
560 fn test_set_path_new_key() {
561 let mut root = make_test_object();
562 set_path(
563 &mut root,
564 "imap.tls",
565 &Value::Bool(true),
566 Addressing::INDEX_ONLY,
567 )
568 .unwrap();
569
570 let result = get_path(&root, "imap.tls", Addressing::INDEX_ONLY).unwrap();
571 assert_eq!(result.as_bool().unwrap(), true);
572 }
573
574 #[test]
575 fn test_set_path_overwrite() {
576 let mut root = make_test_object();
577 set_path(
578 &mut root,
579 "imap.port",
580 &Value::Integer(587),
581 Addressing::INDEX_ONLY,
582 )
583 .unwrap();
584
585 let result = get_path(&root, "imap.port", Addressing::INDEX_ONLY).unwrap();
586 assert_eq!(result.as_integer().unwrap(), 587);
587 }
588
589 #[test]
590 fn test_set_path_array_value() {
591 let mut root = Value::Object(Default::default());
592 let value = Value::Array(vec![
593 Value::String("dev".to_string()),
594 Value::String("staging".to_string()),
595 ]);
596 set_path(&mut root, "tags", &value, Addressing::INDEX_ONLY).unwrap();
597
598 let result = get_path(&root, "tags", Addressing::INDEX_ONLY).unwrap();
599 let arr = result.as_array().unwrap();
600 assert_eq!(arr.len(), 2);
601 }
602
603 fn make_steps_object() -> Value {
604 let mut root = Value::Object(Default::default());
606 let mut s0 = Value::Object(Default::default());
607 s0.as_object_mut()
608 .unwrap()
609 .insert("name".to_string(), Value::String("a".to_string()));
610 s0.as_object_mut()
611 .unwrap()
612 .insert("port".to_string(), Value::Integer(1));
613 let mut s1 = Value::Object(Default::default());
614 s1.as_object_mut()
615 .unwrap()
616 .insert("name".to_string(), Value::String("b".to_string()));
617 s1.as_object_mut()
618 .unwrap()
619 .insert("port".to_string(), Value::Integer(2));
620 root.as_object_mut()
621 .unwrap()
622 .insert("steps".to_string(), Value::Array(vec![s0, s1]));
623 root
624 }
625
626 #[test]
627 fn test_get_path_numeric_index() {
628 let root = make_steps_object();
629 let name = get_path(&root, "steps.0.name", Addressing::INDEX_ONLY).unwrap();
630 assert_eq!(name.as_str().unwrap(), "a");
631 let port = get_path(&root, "steps.1.port", Addressing::INDEX_ONLY).unwrap();
632 assert_eq!(port.as_integer().unwrap(), 2);
633 }
634
635 #[test]
636 fn test_set_path_numeric_index() {
637 let mut root = make_steps_object();
638 set_path(
639 &mut root,
640 "steps.0.port",
641 &Value::Integer(99),
642 Addressing::INDEX_ONLY,
643 )
644 .unwrap();
645 let result = get_path(&root, "steps.0.port", Addressing::INDEX_ONLY).unwrap();
646 assert_eq!(result.as_integer().unwrap(), 99);
647 let other = get_path(&root, "steps.1.port", Addressing::INDEX_ONLY).unwrap();
649 assert_eq!(other.as_integer().unwrap(), 2);
650 }
651
652 #[test]
653 fn test_get_path_index_out_of_bounds() {
654 let root = make_steps_object();
655 let err = get_path(&root, "steps.5.name", Addressing::INDEX_ONLY).unwrap_err();
656 assert!(matches!(
657 err,
658 DocumentError::IndexOutOfBounds {
659 index: 5,
660 len: 2,
661 ..
662 }
663 ));
664 }
665
666 #[test]
667 fn test_remove_path_numeric_index() {
668 let mut root = make_steps_object();
669 unset_path(&mut root, "steps.0").unwrap();
670 let arr = get_path(&root, "steps", Addressing::INDEX_ONLY).unwrap();
671 let arr = arr.as_array().unwrap();
672 assert_eq!(arr.len(), 1);
673 assert_eq!(arr[0].get("name").unwrap().as_str().unwrap(), "b");
674 }
675
676 #[test]
677 fn named_root_array_keeps_a_semantic_prefix_for_nested_keyed_lists() {
678 let mut child = Value::Object(Default::default());
679 child
680 .as_object_mut()
681 .unwrap()
682 .insert("id".to_string(), Value::String("beta".to_string()));
683 child
684 .as_object_mut()
685 .unwrap()
686 .insert("value".to_string(), Value::Integer(1));
687
688 let mut parent = Value::Object(Default::default());
689 parent
690 .as_object_mut()
691 .unwrap()
692 .insert("id".to_string(), Value::String("alpha".to_string()));
693 parent
694 .as_object_mut()
695 .unwrap()
696 .insert("children".to_string(), Value::Array(vec![child]));
697
698 let mut root = Value::Array(vec![parent]);
699 let keyed = [
700 KeyedList {
701 prefix: "",
702 slug_field: "id",
703 },
704 KeyedList {
705 prefix: "alpha.children",
706 slug_field: "id",
707 },
708 ];
709 let addressing = Addressing::keyed(&keyed);
710
711 assert_eq!(
712 get_path(&root, "alpha.children.beta.value", addressing).unwrap(),
713 Value::Integer(1)
714 );
715 set_path(
716 &mut root,
717 "alpha.children.beta.value",
718 &Value::Integer(2),
719 addressing,
720 )
721 .unwrap();
722 assert_eq!(
723 get_path(&root, "alpha.children.beta.value", addressing).unwrap(),
724 Value::Integer(2)
725 );
726 }
727
728 #[test]
729 fn oversized_decimal_array_segment_never_falls_through_to_a_slug() {
730 let oversized = format!("{}0", usize::MAX);
731 let mut item = Value::Object(Default::default());
732 item.as_object_mut()
733 .unwrap()
734 .insert("id".to_string(), Value::String(oversized.clone()));
735 let root = Value::Array(vec![item]);
736 let keyed = [KeyedList {
737 prefix: "",
738 slug_field: "id",
739 }];
740
741 let error = get_path(&root, &oversized, Addressing::keyed(&keyed)).unwrap_err();
742 assert!(matches!(error, DocumentError::PathSyntax { .. }));
743 }
744
745 #[test]
746 fn dotted_key_and_nested_path_keep_distinct_keyed_registrations() {
747 let item = |field: &str, slug: &str| {
748 let mut value = Value::Object(Default::default());
749 value
750 .as_object_mut()
751 .unwrap()
752 .insert(field.to_string(), Value::String(slug.to_string()));
753 value
754 };
755
756 let mut nested = Value::Object(Default::default());
757 nested.as_object_mut().unwrap().insert(
758 "b".to_string(),
759 Value::Array(vec![item("nested_id", "nested")]),
760 );
761
762 let mut root = Value::Object(Default::default());
763 root.as_object_mut().unwrap().insert(
764 "a.b".to_string(),
765 Value::Array(vec![item("dotted_id", "dotted")]),
766 );
767 root.as_object_mut()
768 .unwrap()
769 .insert("a".to_string(), nested);
770
771 let keyed = [
772 KeyedList {
773 prefix: r"a\.b",
774 slug_field: "dotted_id",
775 },
776 KeyedList {
777 prefix: "a.b",
778 slug_field: "nested_id",
779 },
780 ];
781 let addressing = Addressing::keyed(&keyed);
782
783 assert_eq!(
784 get_path(&root, r"a\.b.dotted.dotted_id", addressing).unwrap(),
785 Value::String("dotted".to_string())
786 );
787 assert_eq!(
788 get_path(&root, "a.b.nested.nested_id", addressing).unwrap(),
789 Value::String("nested".to_string())
790 );
791 }
792}