1use serde_yaml_ng::Value;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum PathSeg {
33 Key(String),
35 Index(usize),
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
45pub enum PathError {
46 #[error("manifest path must not be empty")]
48 Empty,
49 #[error("invalid path segment near `{0}`")]
55 BadSegment(String),
56 #[error("cannot descend into scalar at `{0}`")]
60 DescendScalar(String),
61 #[error("index {index} out of bounds (length {len}) at `{at}`")]
65 IndexOutOfBounds {
66 index: usize,
67 len: usize,
68 at: String,
69 },
70 #[error("expected sequence index `[N]` but got key `{key}` at `{at}`")]
74 KeyOnSequence { key: String, at: String },
75 #[error("expected mapping key but got index `[{index}]` at `{at}`")]
78 IndexOnMapping { index: usize, at: String },
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum DeleteOutcome {
86 Removed,
88 NotPresent,
91}
92
93pub fn parse_path(raw: &str) -> Result<Vec<PathSeg>, PathError> {
100 if raw.is_empty() {
101 return Err(PathError::Empty);
102 }
103 let mut segments: Vec<PathSeg> = Vec::new();
104 let mut buf = String::new();
105 let mut chars = raw.chars().peekable();
106 while let Some(c) = chars.next() {
107 match c {
108 '.' => {
109 if buf.is_empty() {
114 if !matches!(segments.last(), Some(PathSeg::Index(_))) {
115 return Err(PathError::BadSegment(raw.to_owned()));
116 }
117 continue;
118 }
119 segments.push(PathSeg::Key(std::mem::take(&mut buf)));
120 }
121 '[' => {
122 if !buf.is_empty() {
124 segments.push(PathSeg::Key(std::mem::take(&mut buf)));
125 }
126 let mut num = String::new();
128 let mut closed = false;
129 for d in chars.by_ref() {
130 if d == ']' {
131 closed = true;
132 break;
133 }
134 num.push(d);
135 }
136 if !closed || num.is_empty() {
137 return Err(PathError::BadSegment(raw.to_owned()));
138 }
139 let idx: usize = num
140 .parse()
141 .map_err(|_| PathError::BadSegment(raw.to_owned()))?;
142 segments.push(PathSeg::Index(idx));
143 if let Some(&peek) = chars.peek() {
147 if peek != '.' && peek != '[' {
148 return Err(PathError::BadSegment(raw.to_owned()));
149 }
150 }
151 }
152 ']' => {
153 return Err(PathError::BadSegment(raw.to_owned()));
155 }
156 other => buf.push(other),
157 }
158 }
159 if !buf.is_empty() {
160 segments.push(PathSeg::Key(buf));
161 }
162 if segments.is_empty() {
163 return Err(PathError::Empty);
164 }
165 Ok(segments)
166}
167
168#[must_use]
175pub fn get_value<'a>(root: &'a Value, path: &[PathSeg]) -> Option<&'a Value> {
176 let mut cur = root;
177 for seg in path {
178 match seg {
179 PathSeg::Key(k) => {
180 cur = cur.as_mapping()?.get(Value::String(k.clone()))?;
181 }
182 PathSeg::Index(i) => {
183 cur = cur.as_sequence()?.get(*i)?;
184 }
185 }
186 }
187 Some(cur)
188}
189
190#[must_use]
202pub fn get_value_json<'a>(
203 root: &'a serde_json::Value,
204 path: &[PathSeg],
205) -> Option<&'a serde_json::Value> {
206 let mut cur = root;
207 for seg in path {
208 match seg {
209 PathSeg::Key(k) => {
210 cur = cur.as_object()?.get(k)?;
211 }
212 PathSeg::Index(i) => {
213 cur = cur.as_array()?.get(*i)?;
214 }
215 }
216 }
217 Some(cur)
218}
219
220pub fn set_value(root: &mut Value, path: &[PathSeg], value: Value) -> Result<(), PathError> {
230 if path.is_empty() {
231 return Err(PathError::Empty);
232 }
233 set_inner(root, path, value, &mut String::new())
234}
235
236fn set_inner(
237 cur: &mut Value,
238 path: &[PathSeg],
239 value: Value,
240 breadcrumb: &mut String,
241) -> Result<(), PathError> {
242 let (head, tail) = path.split_first().expect("non-empty checked by caller");
243 let is_last = tail.is_empty();
244 match head {
245 PathSeg::Key(k) => {
246 push_crumb(breadcrumb, head);
247 if cur.is_null() {
254 *cur = Value::Mapping(serde_yaml_ng::Mapping::new());
255 }
256 let is_sequence = cur.is_sequence();
260 let Some(map) = cur.as_mapping_mut() else {
261 return Err(mapping_kind_mismatch_err(is_sequence, head, breadcrumb));
262 };
263 if is_last {
264 map.insert(Value::String(k.clone()), value);
265 return Ok(());
266 }
267 let key = Value::String(k.clone());
276 if !map.contains_key(&key) {
277 map.insert(key.clone(), empty_for_next(tail));
278 }
279 let next = map.get_mut(&key).expect("just inserted");
280 set_inner(next, tail, value, breadcrumb)
281 }
282 PathSeg::Index(i) => {
283 push_crumb(breadcrumb, head);
284 let is_mapping = cur.is_mapping();
285 let Some(seq) = cur.as_sequence_mut() else {
286 return Err(sequence_kind_mismatch_err(is_mapping, head, breadcrumb));
287 };
288 let len = seq.len();
289 if *i > len {
290 return Err(PathError::IndexOutOfBounds {
291 index: *i,
292 len,
293 at: breadcrumb.clone(),
294 });
295 }
296 if is_last {
297 if *i == len {
298 seq.push(value);
299 } else {
300 seq[*i] = value;
301 }
302 return Ok(());
303 }
304 if *i == len {
305 seq.push(empty_for_next(tail));
313 }
314 set_inner(&mut seq[*i], tail, value, breadcrumb)
315 }
316 }
317}
318
319fn empty_for_next(tail: &[PathSeg]) -> Value {
330 match tail.first() {
331 Some(PathSeg::Index(_)) => Value::Sequence(serde_yaml_ng::Sequence::new()),
332 _ => Value::Mapping(serde_yaml_ng::Mapping::new()),
333 }
334}
335
336pub fn delete_value(root: &mut Value, path: &[PathSeg]) -> Result<DeleteOutcome, PathError> {
340 if path.is_empty() {
341 return Err(PathError::Empty);
342 }
343 delete_inner(root, path, &mut String::new())
344}
345
346fn delete_inner(
347 cur: &mut Value,
348 path: &[PathSeg],
349 breadcrumb: &mut String,
350) -> Result<DeleteOutcome, PathError> {
351 let (head, tail) = path.split_first().expect("non-empty checked by caller");
352 let is_last = tail.is_empty();
353 match head {
354 PathSeg::Key(k) => {
355 push_crumb(breadcrumb, head);
356 let is_null = cur.is_null();
358 let is_sequence = cur.is_sequence();
359 let Some(map) = cur.as_mapping_mut() else {
360 if is_null {
364 return Ok(DeleteOutcome::NotPresent);
365 }
366 return Err(mapping_kind_mismatch_err(is_sequence, head, breadcrumb));
367 };
368 let key = Value::String(k.clone());
369 if is_last {
370 return Ok(if map.remove(&key).is_some() {
371 DeleteOutcome::Removed
372 } else {
373 DeleteOutcome::NotPresent
374 });
375 }
376 let Some(next) = map.get_mut(&key) else {
377 return Ok(DeleteOutcome::NotPresent);
378 };
379 delete_inner(next, tail, breadcrumb)
380 }
381 PathSeg::Index(i) => {
382 push_crumb(breadcrumb, head);
383 let is_null = cur.is_null();
384 let is_mapping = cur.is_mapping();
385 let Some(seq) = cur.as_sequence_mut() else {
386 if is_null {
387 return Ok(DeleteOutcome::NotPresent);
388 }
389 return Err(sequence_kind_mismatch_err(is_mapping, head, breadcrumb));
390 };
391 if *i >= seq.len() {
392 return Ok(DeleteOutcome::NotPresent);
393 }
394 if is_last {
395 seq.remove(*i);
396 return Ok(DeleteOutcome::Removed);
397 }
398 delete_inner(&mut seq[*i], tail, breadcrumb)
399 }
400 }
401}
402
403fn push_crumb(breadcrumb: &mut String, seg: &PathSeg) {
404 match seg {
405 PathSeg::Key(k) => {
406 if !breadcrumb.is_empty() {
407 breadcrumb.push('.');
408 }
409 breadcrumb.push_str(k);
410 }
411 PathSeg::Index(i) => {
412 use std::fmt::Write;
413 let _ = write!(breadcrumb, "[{i}]");
414 }
415 }
416}
417
418fn mapping_kind_mismatch_err(
428 parent_is_sequence: bool,
429 seg: &PathSeg,
430 breadcrumb: &str,
431) -> PathError {
432 let at = trim_one_segment(breadcrumb, seg);
433 if parent_is_sequence {
434 if let PathSeg::Key(k) = seg {
435 return PathError::KeyOnSequence { key: k.clone(), at };
436 }
437 }
438 PathError::DescendScalar(at)
439}
440
441fn sequence_kind_mismatch_err(
445 parent_is_mapping: bool,
446 seg: &PathSeg,
447 breadcrumb: &str,
448) -> PathError {
449 let at = trim_one_segment(breadcrumb, seg);
450 if parent_is_mapping {
451 if let PathSeg::Index(i) = seg {
452 return PathError::IndexOnMapping { index: *i, at };
453 }
454 }
455 PathError::DescendScalar(at)
456}
457
458fn trim_one_segment(breadcrumb: &str, seg: &PathSeg) -> String {
461 match seg {
462 PathSeg::Key(k) => {
463 let with_dot = format!(".{k}");
464 breadcrumb.strip_suffix(&with_dot).map_or_else(
465 || breadcrumb.trim_start_matches(k).to_owned(),
466 str::to_owned,
467 )
468 }
469 PathSeg::Index(i) => {
470 let bracketed = format!("[{i}]");
471 breadcrumb
472 .strip_suffix(&bracketed)
473 .map_or_else(|| breadcrumb.to_owned(), str::to_owned)
474 }
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use serde_yaml_ng::Value;
482
483 fn sample() -> Value {
484 serde_yaml_ng::from_str(
485 "name: demo\nversion: 0.1.0\ndescription: a demo\ndependencies:\n skills:\n - alice/bob@0.1.0\n - carol/dave\n mcp:\n - registry: official\n name: filesystem\n",
486 )
487 .unwrap()
488 }
489
490 #[test]
491 fn parse_path_handles_keys_and_indices() {
492 assert_eq!(
493 parse_path("name").unwrap(),
494 vec![PathSeg::Key("name".into())]
495 );
496 assert_eq!(
497 parse_path("dependencies.skills[0]").unwrap(),
498 vec![
499 PathSeg::Key("dependencies".into()),
500 PathSeg::Key("skills".into()),
501 PathSeg::Index(0),
502 ]
503 );
504 assert_eq!(
505 parse_path("dependencies.mcp[1].agents").unwrap(),
506 vec![
507 PathSeg::Key("dependencies".into()),
508 PathSeg::Key("mcp".into()),
509 PathSeg::Index(1),
510 PathSeg::Key("agents".into()),
511 ]
512 );
513 assert_eq!(parse_path("[0]").unwrap(), vec![PathSeg::Index(0)]);
514 }
515
516 #[test]
517 fn parse_path_rejects_malformed_input() {
518 assert!(matches!(parse_path("").unwrap_err(), PathError::Empty));
519 assert!(matches!(
520 parse_path(".foo").unwrap_err(),
521 PathError::BadSegment(_)
522 ));
523 assert!(matches!(
524 parse_path("a..b").unwrap_err(),
525 PathError::BadSegment(_)
526 ));
527 assert!(matches!(
528 parse_path("a[").unwrap_err(),
529 PathError::BadSegment(_)
530 ));
531 assert!(matches!(
532 parse_path("a[]").unwrap_err(),
533 PathError::BadSegment(_)
534 ));
535 assert!(matches!(
536 parse_path("a[abc]").unwrap_err(),
537 PathError::BadSegment(_)
538 ));
539 assert!(matches!(
540 parse_path("a]b").unwrap_err(),
541 PathError::BadSegment(_)
542 ));
543 assert!(matches!(
544 parse_path("a[0]b").unwrap_err(),
545 PathError::BadSegment(_)
546 ));
547 }
548
549 #[test]
550 fn get_value_resolves_keys_indices_and_returns_none_on_miss() {
551 let root = sample();
552 let path = parse_path("description").unwrap();
553 assert_eq!(get_value(&root, &path).unwrap().as_str(), Some("a demo"));
554
555 let path = parse_path("dependencies.skills[0]").unwrap();
556 assert_eq!(
557 get_value(&root, &path).unwrap().as_str(),
558 Some("alice/bob@0.1.0")
559 );
560
561 let path = parse_path("dependencies.skills[99]").unwrap();
562 assert!(get_value(&root, &path).is_none());
563
564 let path = parse_path("nope").unwrap();
565 assert!(get_value(&root, &path).is_none());
566 }
567
568 #[test]
569 fn set_value_overwrites_existing_scalar() {
570 let mut root = sample();
571 let path = parse_path("description").unwrap();
572 set_value(&mut root, &path, Value::String("new desc".into())).unwrap();
573 assert_eq!(get_value(&root, &path).unwrap().as_str(), Some("new desc"));
574 }
575
576 #[test]
577 fn set_value_pushes_when_index_equals_len() {
578 let mut root = sample();
579 let path = parse_path("dependencies.skills[2]").unwrap();
580 set_value(&mut root, &path, Value::String("eve/frank@0.2.0".into())).unwrap();
581 assert_eq!(
582 get_value(&root, &path).unwrap().as_str(),
583 Some("eve/frank@0.2.0")
584 );
585 let p0 = parse_path("dependencies.skills[0]").unwrap();
587 assert_eq!(
588 get_value(&root, &p0).unwrap().as_str(),
589 Some("alice/bob@0.1.0")
590 );
591 }
592
593 #[test]
594 fn set_value_rejects_gap_past_len() {
595 let mut root = sample();
596 let path = parse_path("dependencies.skills[5]").unwrap();
597 let err = set_value(&mut root, &path, Value::String("x".into())).unwrap_err();
598 assert!(matches!(err, PathError::IndexOutOfBounds { .. }));
599 }
600
601 #[test]
602 fn set_value_creates_intermediate_mappings_on_fresh_root() {
603 let mut root: Value = serde_yaml_ng::from_str("name: demo\nversion: 0.1.0\n").unwrap();
604 let path = parse_path("metadata.repo.url").unwrap();
605 set_value(
606 &mut root,
607 &path,
608 Value::String("https://example.test".into()),
609 )
610 .unwrap();
611 assert_eq!(
612 get_value(&root, &path).unwrap().as_str(),
613 Some("https://example.test")
614 );
615 }
616
617 #[test]
629 fn set_value_scaffolds_sequences_when_next_segment_is_index() {
630 let mut root: Value = serde_yaml_ng::from_str("name: demo\n").unwrap();
632 let path = parse_path("foo[0][0]").unwrap();
633 set_value(&mut root, &path, Value::String("bar".into())).unwrap();
634
635 let foo = root
637 .as_mapping()
638 .unwrap()
639 .get(Value::String("foo".into()))
640 .unwrap();
641 let outer = foo.as_sequence().expect("foo must be a sequence");
642 assert_eq!(outer.len(), 1);
643 let inner = outer[0]
644 .as_sequence()
645 .expect("foo[0] must be a sequence (auto-extended on Index recursion)");
646 assert_eq!(inner.len(), 1);
647 assert_eq!(inner[0].as_str(), Some("bar"));
648 }
649
650 #[test]
656 fn set_value_scaffolds_mapping_after_index_when_next_is_key() {
657 let mut root: Value = serde_yaml_ng::from_str("name: demo\n").unwrap();
658 let path = parse_path("foo[0].name").unwrap();
659 set_value(&mut root, &path, Value::String("bar".into())).unwrap();
660
661 let foo = root
662 .as_mapping()
663 .unwrap()
664 .get(Value::String("foo".into()))
665 .unwrap();
666 let outer = foo.as_sequence().expect("foo must be a sequence");
667 assert_eq!(outer.len(), 1);
668 let elem = outer[0].as_mapping().expect("foo[0] must be a mapping");
669 let name = elem.get(Value::String("name".into())).unwrap();
670 assert_eq!(name.as_str(), Some("bar"));
671 }
672
673 #[test]
674 fn set_value_refuses_to_descend_into_scalar() {
675 let mut root = sample();
676 let path = parse_path("description.foo").unwrap();
677 let err = set_value(&mut root, &path, Value::String("x".into())).unwrap_err();
678 assert!(matches!(
679 err,
680 PathError::DescendScalar(_) | PathError::KeyOnSequence { .. }
681 ));
682 }
683
684 #[test]
685 fn delete_value_removes_existing_key() {
686 let mut root = sample();
687 let path = parse_path("description").unwrap();
688 assert_eq!(
689 delete_value(&mut root, &path).unwrap(),
690 DeleteOutcome::Removed
691 );
692 assert!(get_value(&root, &path).is_none());
693 }
694
695 #[test]
696 fn delete_value_removes_existing_index_and_shifts_remaining() {
697 let mut root = sample();
698 let path = parse_path("dependencies.skills[0]").unwrap();
699 assert_eq!(
700 delete_value(&mut root, &path).unwrap(),
701 DeleteOutcome::Removed
702 );
703 let p0 = parse_path("dependencies.skills[0]").unwrap();
705 assert_eq!(get_value(&root, &p0).unwrap().as_str(), Some("carol/dave"));
706 let p1 = parse_path("dependencies.skills[1]").unwrap();
708 assert!(get_value(&root, &p1).is_none());
709 }
710
711 #[test]
712 fn delete_value_returns_not_present_for_missing_path() {
713 let mut root = sample();
714 let path = parse_path("missing.deep.key").unwrap();
715 assert_eq!(
716 delete_value(&mut root, &path).unwrap(),
717 DeleteOutcome::NotPresent
718 );
719 }
720
721 #[test]
722 fn delete_value_returns_not_present_for_out_of_bounds_index() {
723 let mut root = sample();
724 let path = parse_path("dependencies.skills[99]").unwrap();
725 assert_eq!(
726 delete_value(&mut root, &path).unwrap(),
727 DeleteOutcome::NotPresent
728 );
729 }
730
731 #[test]
732 fn get_value_json_resolves_keys_indices_and_scalars() {
733 let root: serde_json::Value = serde_json::json!({
734 "id": "alice/hello",
735 "description": "a demo",
736 "versions": [
737 {"version": "0.1.0", "sha256": "aaa"},
738 {"version": "0.1.1", "sha256": "bbb"},
739 ],
740 });
741 let p = parse_path("description").unwrap();
742 assert_eq!(get_value_json(&root, &p).unwrap().as_str(), Some("a demo"));
743
744 let p = parse_path("versions[1].version").unwrap();
745 assert_eq!(get_value_json(&root, &p).unwrap().as_str(), Some("0.1.1"));
746
747 let p = parse_path("versions[0]").unwrap();
748 assert!(get_value_json(&root, &p).unwrap().is_object());
749
750 let p = parse_path("versions").unwrap();
751 assert!(get_value_json(&root, &p).unwrap().is_array());
752 }
753
754 #[test]
755 fn get_value_json_returns_none_on_miss() {
756 let root: serde_json::Value = serde_json::json!({
757 "id": "alice/hello",
758 "versions": [{"version": "0.1.0"}],
759 });
760 let p = parse_path("nope").unwrap();
762 assert!(get_value_json(&root, &p).is_none());
763 let p = parse_path("versions[99]").unwrap();
765 assert!(get_value_json(&root, &p).is_none());
766 let p = parse_path("id.deep").unwrap();
768 assert!(get_value_json(&root, &p).is_none());
769 }
770}