1use jsonc_parser::ast::{Array, Object, ObjectPropName, Value};
16use jsonc_parser::common::Ranged;
17use jsonc_parser::{CollectOptions, ParseOptions, parse_to_ast};
18use serde_json::json;
19use yaml_edit::path::YamlPath;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Seg {
25 Key(String),
26 Index(usize),
27 Select { key: String, value: String },
28}
29
30pub fn parse_path(spec: &str) -> Result<Vec<Seg>, String> {
55 let body = spec.strip_prefix('.').unwrap_or(spec);
56 if body.is_empty() {
57 return Err(format!("empty path '{spec}'"));
58 }
59 let mut segs = Vec::new();
60 for part in body.split('.') {
61 let mut rest = part;
62 match rest.find('[') {
63 Some(br) => {
64 let key = &rest[..br];
65 if !key.is_empty() {
66 segs.push(Seg::Key(key.to_string()));
67 }
68 rest = &rest[br..];
69 while let Some(stripped) = rest.strip_prefix('[') {
70 let close = stripped
71 .find(']')
72 .ok_or_else(|| format!("unclosed '[' in path '{spec}'"))?;
73 let inner = &stripped[..close];
74 if let Some((k, v)) = inner.split_once('=') {
75 segs.push(Seg::Select {
76 key: k.to_string(),
77 value: v.to_string(),
78 });
79 } else {
80 let idx: usize = inner
81 .parse()
82 .map_err(|_| format!("invalid index '[{inner}]' in path '{spec}'"))?;
83 segs.push(Seg::Index(idx));
84 }
85 rest = &stripped[close + 1..];
86 }
87 if !rest.is_empty() {
88 return Err(format!("trailing characters '{rest}' in path '{spec}'"));
89 }
90 }
91 None => {
92 if rest.is_empty() {
93 return Err(format!("empty segment in path '{spec}'"));
94 }
95 segs.push(Seg::Key(rest.to_string()));
96 }
97 }
98 }
99 Ok(segs)
100}
101
102pub fn split_assign(spec: &str) -> Option<(&str, &str)> {
118 let mut depth = 0i32;
119 for (i, c) in spec.char_indices() {
120 match c {
121 '[' => depth += 1,
122 ']' => depth = (depth - 1).max(0),
123 '=' if depth == 0 => return Some((&spec[..i], &spec[i + 1..])),
124 _ => {}
125 }
126 }
127 None
128}
129
130pub fn normalize_value(v: &str) -> String {
144 match serde_json::from_str::<serde_json::Value>(v) {
145 Ok(parsed) => parsed.to_string(),
146 Err(_) => serde_json::Value::String(v.to_string()).to_string(),
147 }
148}
149
150#[derive(Debug, Clone, Copy)]
152pub enum MoveTo {
153 First,
154 Last,
155 Up,
156 Down,
157}
158
159pub enum Op {
161 Set {
162 path: Vec<Seg>,
163 raw: String,
164 value: String,
165 },
166 Add {
167 path: Vec<Seg>,
168 raw: String,
169 value: String,
170 },
171 Delete {
172 path: Vec<Seg>,
173 raw: String,
174 },
175 Move {
176 path: Vec<Seg>,
177 raw: String,
178 to: MoveTo,
179 },
180}
181
182fn prop_key(name: &ObjectPropName) -> String {
184 match name {
185 ObjectPropName::String(s) => s.value.to_string(),
186 ObjectPropName::Word(w) => w.value.to_string(),
187 }
188}
189
190fn scalar_eq(node: &Value, value: &str) -> bool {
192 match node {
193 Value::StringLit(s) => s.value.as_ref() == value,
194 Value::NumberLit(n) => n.value == value,
195 Value::BooleanLit(b) => (if b.value { "true" } else { "false" }) == value,
196 Value::NullKeyword(_) => value == "null",
197 _ => false,
198 }
199}
200
201fn select_index(arr: &Array, key: &str, value: &str) -> Option<usize> {
203 arr.elements.iter().position(|e| match e {
204 Value::Object(o) => o
205 .properties
206 .iter()
207 .any(|p| prop_key(&p.name) == key && scalar_eq(&p.value, value)),
208 _ => false,
209 })
210}
211
212fn final_array_index(parent: &Value, last: &Seg, raw: &str) -> Result<usize, String> {
215 let arr = as_array(parent, raw)?;
216 match last {
217 Seg::Index(i) => Ok(*i),
218 Seg::Select { key, value } => select_index(arr, key, value)
219 .ok_or_else(|| format!("path '{raw}': no array element where {key}={value}")),
220 Seg::Key(_) => Err(format!(
221 "path '{raw}': expected an array element, got an object key"
222 )),
223 }
224}
225
226fn line_indent(text: &str, pos: usize) -> String {
228 let line_start = text[..pos].rfind('\n').map(|i| i + 1).unwrap_or(0);
229 text[line_start..pos]
230 .chars()
231 .take_while(|c| *c == ' ' || *c == '\t')
232 .collect()
233}
234
235fn splice(text: &str, start: usize, end: usize, repl: &str) -> (String, bool) {
237 let out = format!("{}{}{}", &text[..start], repl, &text[end..]);
238 let changed = out != text;
239 (out, changed)
240}
241
242fn navigate<'a>(root: &'a Value<'a>, segs: &[Seg]) -> Option<&'a Value<'a>> {
244 let mut cur = root;
245 for seg in segs {
246 cur = match (seg, cur) {
247 (Seg::Key(k), Value::Object(o)) => {
248 &o.properties.iter().find(|p| prop_key(&p.name) == *k)?.value
249 }
250 (Seg::Index(i), Value::Array(a)) => a.elements.get(*i)?,
251 (Seg::Select { key, value }, Value::Array(a)) => {
252 a.elements.get(select_index(a, key, value)?)?
253 }
254 _ => return None,
255 };
256 }
257 Some(cur)
258}
259
260fn append_into(
263 text: &str,
264 open: usize,
265 first_child: usize,
266 last_end: usize,
267 entry: &str,
268) -> (String, bool) {
269 let multiline = text[open + 1..first_child].contains('\n');
270 let insert = if multiline {
271 format!(",\n{}{entry}", line_indent(text, first_child))
272 } else {
273 format!(", {entry}")
274 };
275 splice(text, last_end, last_end, &insert)
276}
277
278fn set_in(
280 text: &str,
281 root: &Value,
282 path: &[Seg],
283 value: &str,
284 raw: &str,
285) -> Result<(String, bool), String> {
286 let (last, parents) = path.split_last().expect("path is non-empty");
287 let parent = navigate(root, parents)
288 .ok_or_else(|| format!("path '{raw}' not found (a parent segment is missing)"))?;
289 match last {
290 Seg::Key(k) => {
291 let obj = as_object(parent, raw)?;
292 if let Some(p) = obj.properties.iter().find(|p| prop_key(&p.name) == *k) {
293 let r = p.value.range();
294 Ok(splice(text, r.start, r.end, value))
295 } else {
296 Ok(add_key(text, obj, k, value))
297 }
298 }
299 Seg::Index(i) => {
300 let arr = as_array(parent, raw)?;
301 let len = arr.elements.len();
302 if *i < len {
303 let r = arr.elements[*i].range();
304 Ok(splice(text, r.start, r.end, value))
305 } else if *i == len {
306 Ok(append_elem(text, arr, value))
307 } else {
308 Err(format!(
309 "path '{raw}': index {i} is out of range (length {len})"
310 ))
311 }
312 }
313 Seg::Select { key, value: want } => {
314 let arr = as_array(parent, raw)?;
315 let i = select_index(arr, key, want)
316 .ok_or_else(|| format!("path '{raw}': no array element where {key}={want}"))?;
317 let r = arr.elements[i].range();
318 Ok(splice(text, r.start, r.end, value))
319 }
320 }
321}
322
323fn add_key(text: &str, obj: &Object, key: &str, value: &str) -> (String, bool) {
325 let entry = format!("{}: {value}", json!(key));
326 if obj.properties.is_empty() {
327 let pos = obj.range().start + 1;
328 return splice(text, pos, pos, &entry);
329 }
330 let first = obj.properties[0].range().start;
331 let last_end = obj.properties.last().unwrap().range().end;
332 append_into(text, obj.range().start, first, last_end, &entry)
333}
334
335fn append_elem(text: &str, arr: &Array, value: &str) -> (String, bool) {
337 if arr.elements.is_empty() {
338 let pos = arr.range().start + 1;
339 return splice(text, pos, pos, value);
340 }
341 let first = arr.elements[0].range().start;
342 let last_end = arr.elements.last().unwrap().range().end;
343 append_into(text, arr.range().start, first, last_end, value)
344}
345
346fn delete_in(text: &str, root: &Value, path: &[Seg], raw: &str) -> Result<(String, bool), String> {
348 let (last, parents) = path.split_last().expect("path is non-empty");
349 let parent = match navigate(root, parents) {
350 Some(p) => p,
351 None => return Ok((text.to_string(), false)),
352 };
353 match last {
354 Seg::Key(k) => {
355 let obj = as_object(parent, raw)?;
356 match obj.properties.iter().position(|p| prop_key(&p.name) == *k) {
357 Some(idx) => Ok(delete_member(
358 text,
359 obj.range().start,
360 obj.range().end,
361 &obj.properties
362 .iter()
363 .map(|p| (p.range().start, p.range().end))
364 .collect::<Vec<_>>(),
365 idx,
366 "{}",
367 )),
368 None => Ok((text.to_string(), false)),
369 }
370 }
371 Seg::Index(i) => {
372 let arr = as_array(parent, raw)?;
373 if *i < arr.elements.len() {
374 Ok(delete_member(
375 text,
376 arr.range().start,
377 arr.range().end,
378 &arr.elements
379 .iter()
380 .map(|e| (e.range().start, e.range().end))
381 .collect::<Vec<_>>(),
382 *i,
383 "[]",
384 ))
385 } else {
386 Ok((text.to_string(), false))
387 }
388 }
389 Seg::Select { key, value: want } => {
390 let arr = as_array(parent, raw)?;
391 match select_index(arr, key, want) {
392 Some(i) => Ok(delete_member(
393 text,
394 arr.range().start,
395 arr.range().end,
396 &arr.elements
397 .iter()
398 .map(|e| (e.range().start, e.range().end))
399 .collect::<Vec<_>>(),
400 i,
401 "[]",
402 )),
403 None => Ok((text.to_string(), false)),
404 }
405 }
406 }
407}
408
409fn delete_member(
412 text: &str,
413 open: usize,
414 close: usize,
415 members: &[(usize, usize)],
416 idx: usize,
417 empty: &str,
418) -> (String, bool) {
419 if members.len() == 1 {
420 return splice(text, open, close, empty);
421 }
422 let (start, end) = if idx + 1 < members.len() {
423 (members[idx].0, members[idx + 1].0)
426 } else {
427 (members[idx - 1].1, members[idx].1)
429 };
430 splice(text, start, end, "")
431}
432
433fn as_object<'a>(node: &'a Value<'a>, raw: &str) -> Result<&'a Object<'a>, String> {
434 match node {
435 Value::Object(o) => Ok(o),
436 _ => Err(format!("path '{raw}': expected an object")),
437 }
438}
439
440fn as_array<'a>(node: &'a Value<'a>, raw: &str) -> Result<&'a Array<'a>, String> {
441 match node {
442 Value::Array(a) => Ok(a),
443 _ => Err(format!("path '{raw}': expected an array")),
444 }
445}
446
447fn add_in(
449 text: &str,
450 root: &Value,
451 path: &[Seg],
452 value: &str,
453 raw: &str,
454) -> Result<(String, bool), String> {
455 let node = navigate(root, path).ok_or_else(|| format!("path '{raw}' not found"))?;
456 let arr = as_array(node, raw)?;
457 Ok(append_elem(text, arr, value))
458}
459
460fn move_in(
463 text: &str,
464 root: &Value,
465 path: &[Seg],
466 to: MoveTo,
467 raw: &str,
468) -> Result<(String, bool), String> {
469 let (last, parents) = path
470 .split_last()
471 .ok_or_else(|| format!("empty path '{raw}'"))?;
472 let parent = navigate(root, parents).ok_or_else(|| format!("path '{raw}' not found"))?;
473 let arr = as_array(parent, raw)?;
474 let i = final_array_index(parent, last, raw)?;
475 let len = arr.elements.len();
476 if i >= len {
477 return Err(format!(
478 "path '{raw}': index {i} is out of range (length {len})"
479 ));
480 }
481 if len < 2 {
482 return Ok((text.to_string(), false));
483 }
484 let j = match to {
485 MoveTo::First => 0,
486 MoveTo::Last => len - 1,
487 MoveTo::Up => i.saturating_sub(1),
488 MoveTo::Down => (i + 1).min(len - 1),
489 };
490 if i == j {
491 return Ok((text.to_string(), false));
492 }
493 let spans: Vec<(usize, usize)> = arr
494 .elements
495 .iter()
496 .map(|e| {
497 let r = e.range();
498 (r.start, r.end)
499 })
500 .collect();
501 let items: Vec<&str> = spans.iter().map(|&(s, e)| &text[s..e]).collect();
502 let mut order: Vec<usize> = (0..len).collect();
503 let moved = order.remove(i);
504 order.insert(j, moved);
505 let sep = text[spans[0].1..spans[1].0].to_string();
507 let reordered: Vec<&str> = order.iter().map(|&k| items[k]).collect();
508 Ok(splice(
509 text,
510 spans[0].0,
511 spans[len - 1].1,
512 &reordered.join(&sep),
513 ))
514}
515
516pub fn apply_op(text: &str, op: &Op) -> Result<(String, bool), String> {
519 let parsed = parse_to_ast(text, &CollectOptions::default(), &ParseOptions::default())
520 .map_err(|e| format!("parse error: {e}"))?;
521 let root = parsed.value.as_ref().ok_or("document is empty")?;
522 match op {
523 Op::Set { path, value, raw } => set_in(text, root, path, value, raw),
524 Op::Add { path, value, raw } => add_in(text, root, path, value, raw),
525 Op::Delete { path, raw } => delete_in(text, root, path, raw),
526 Op::Move { path, to, raw } => move_in(text, root, path, *to, raw),
527 }
528}
529
530pub fn apply_doc(text: &str, ops: &[Op]) -> Result<(String, usize), String> {
553 let mut cur = text.to_string();
554 let mut changes = 0usize;
555 for op in ops {
556 let (next, changed) = apply_op(&cur, op)?;
557 if changed {
558 changes += 1;
559 }
560 cur = next;
561 }
562 Ok((cur, changes))
563}
564
565pub fn apply_jsonl(text: &str, ops: &[Op]) -> Result<(String, usize), String> {
567 let mut out = String::with_capacity(text.len());
568 let mut changes = 0usize;
569 for segment in text.split_inclusive('\n') {
570 let (body, nl) = match segment.strip_suffix('\n') {
571 Some(b) => (b, "\n"),
572 None => (segment, ""),
573 };
574 if body.trim().is_empty() {
575 out.push_str(segment);
576 continue;
577 }
578 let (patched, n) = apply_doc(body, ops)?;
579 changes += n;
580 out.push_str(&patched);
581 out.push_str(nl);
582 }
583 Ok((out, changes))
584}
585
586fn key_path(path: &[Seg], raw: &str) -> Result<Vec<String>, String> {
589 path.iter()
590 .map(|s| match s {
591 Seg::Key(k) => Ok(k.clone()),
592 Seg::Index(_) => Err(format!(
593 "array-index paths are not yet supported for YAML ('{raw}')"
594 )),
595 Seg::Select { .. } => Err(format!(
596 "predicate paths are not yet supported for YAML ('{raw}')"
597 )),
598 })
599 .collect()
600}
601
602fn leading_trivia(text: &str) -> String {
605 let mut end = 0;
606 for line in text.split_inclusive('\n') {
607 let trimmed = line.trim_start();
608 if trimmed.is_empty() || trimmed.starts_with('#') {
609 end += line.len();
610 } else {
611 break;
612 }
613 }
614 text[..end].to_string()
615}
616
617fn yaml_set(doc: &yaml_edit::Document, dotted: &str, value: &str, raw: &str) -> Result<(), String> {
620 let parsed: serde_json::Value = serde_json::from_str(value)
622 .unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
623 match parsed {
624 serde_json::Value::Bool(b) => doc.set_path(dotted, b),
625 serde_json::Value::String(s) => doc.set_path(dotted, s.as_str()),
626 serde_json::Value::Number(n) => {
627 if let Some(i) = n.as_i64() {
628 doc.set_path(dotted, i);
629 } else if let Some(u) = n.as_u64() {
630 doc.set_path(dotted, u);
631 } else {
632 doc.set_path(dotted, n.as_f64().unwrap());
633 }
634 }
635 serde_json::Value::Null => {
636 return Err(format!(
637 "path '{raw}': null values are not yet supported for YAML"
638 ));
639 }
640 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
641 return Err(format!(
642 "path '{raw}': array/object values are not yet supported for YAML"
643 ));
644 }
645 }
646 Ok(())
647}
648
649pub fn apply_yaml(text: &str, ops: &[Op]) -> Result<(String, usize), String> {
673 use std::str::FromStr;
674 let doc = yaml_edit::Document::from_str(text).map_err(|e| format!("yaml parse error: {e}"))?;
675 let mut changes = 0usize;
676 for op in ops {
677 let before = doc.to_string();
678 match op {
679 Op::Set { path, value, raw } => {
680 let keys = key_path(path, raw)?;
681 let dotted = keys.join(".");
682 if doc.get_path(&dotted).is_none() {
685 return Err(format!(
686 "path '{raw}': key does not exist (adding new YAML keys is handled by the insert verbs)"
687 ));
688 }
689 yaml_set(&doc, &dotted, value, raw)?;
690 }
691 Op::Delete { path, raw } => {
692 let keys = key_path(path, raw)?;
693 let (last, parents) = keys
694 .split_last()
695 .ok_or_else(|| format!("empty path '{raw}'"))?;
696 let mut map = doc
697 .as_mapping()
698 .ok_or_else(|| format!("path '{raw}': document root is not a mapping"))?;
699 for k in parents {
700 map = map
701 .get_mapping(k)
702 .ok_or_else(|| format!("path '{raw}': '{k}' is not a mapping"))?;
703 }
704 map.remove(last);
705 }
706 Op::Add { raw, .. } => {
709 return Err(format!("--add is not yet supported for YAML ('{raw}')"));
710 }
711 Op::Move { raw, .. } => {
712 return Err(format!("--move-* is not yet supported for YAML ('{raw}')"));
713 }
714 }
715 if doc.to_string() != before {
716 changes += 1;
717 }
718 }
719 let leading = leading_trivia(text);
721 let mut out = doc.to_string();
722 if !leading.is_empty() && !out.starts_with(&leading) {
723 out = format!("{leading}{out}");
724 }
725 Ok((out, changes))
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 fn set(text: &str, path: &str, value: &str) -> (String, bool) {
733 apply_op(
734 text,
735 &Op::Set {
736 path: parse_path(path).unwrap(),
737 raw: path.to_string(),
738 value: normalize_value(value),
739 },
740 )
741 .unwrap()
742 }
743
744 fn delete(text: &str, path: &str) -> (String, bool) {
745 apply_op(
746 text,
747 &Op::Delete {
748 path: parse_path(path).unwrap(),
749 raw: path.to_string(),
750 },
751 )
752 .unwrap()
753 }
754
755 fn add(text: &str, path: &str, value: &str) -> (String, bool) {
756 apply_op(
757 text,
758 &Op::Add {
759 path: parse_path(path).unwrap(),
760 raw: path.to_string(),
761 value: normalize_value(value),
762 },
763 )
764 .unwrap()
765 }
766
767 fn move_el(text: &str, path: &str, to: MoveTo) -> (String, bool) {
768 apply_op(
769 text,
770 &Op::Move {
771 path: parse_path(path).unwrap(),
772 raw: path.to_string(),
773 to,
774 },
775 )
776 .unwrap()
777 }
778
779 #[test]
780 fn path_parsing() {
781 assert_eq!(
782 parse_path(".a.b").unwrap(),
783 vec![Seg::Key("a".into()), Seg::Key("b".into())]
784 );
785 assert_eq!(parse_path("[0]").unwrap(), vec![Seg::Index(0)]);
786 assert_eq!(
787 parse_path("a[name=web].port").unwrap(),
788 vec![
789 Seg::Key("a".into()),
790 Seg::Select {
791 key: "name".into(),
792 value: "web".into()
793 },
794 Seg::Key("port".into()),
795 ]
796 );
797 assert!(parse_path("").is_err());
798 assert!(parse_path("a[x]").is_err());
799 }
800
801 #[test]
802 fn assign_splits_outside_brackets() {
803 assert_eq!(split_assign(".a[n=b].c=1"), Some((".a[n=b].c", "1")));
804 assert_eq!(split_assign(".x=hi"), Some((".x", "hi")));
805 assert_eq!(split_assign(".x"), None);
806 }
807
808 #[test]
809 fn value_normalisation() {
810 assert_eq!(normalize_value("42"), "42");
811 assert_eq!(normalize_value("name"), "\"name\"");
812 }
813
814 #[test]
815 fn set_replaces_value_preserving_comments_and_layout() {
816 let t = "{\n \"a\": 1, // keep me\n \"b\": 2\n}\n";
817 let (out, changed) = set(t, ".a", "42");
818 assert!(changed);
819 assert_eq!(out, "{\n \"a\": 42, // keep me\n \"b\": 2\n}\n");
820 }
821
822 #[test]
823 fn set_adds_missing_key_multiline_with_indent() {
824 let (out, _) = set("{\n \"a\": 1\n}\n", ".b", "true");
825 assert_eq!(out, "{\n \"a\": 1,\n \"b\": true\n}\n");
826 }
827
828 #[test]
829 fn set_string_fallback_quotes_value() {
830 let (out, _) = set("{\"a\":1}", ".a", "hello");
831 assert_eq!(out, "{\"a\":\"hello\"}");
832 }
833
834 #[test]
835 fn nested_set_and_array_index() {
836 let t = "{\n \"x\": { \"y\": [10, 20, 30] }\n}\n";
837 let (out, changed) = set(t, ".x.y[1]", "99");
838 assert!(changed);
839 assert_eq!(out, "{\n \"x\": { \"y\": [10, 99, 30] }\n}\n");
840 }
841
842 #[test]
843 fn append_array_element_via_index() {
844 let (out, _) = set("[1, 2]", "[2]", "3"); assert_eq!(out, "[1, 2, 3]");
846 }
847
848 #[test]
849 fn predicate_selects_object_in_array() {
850 let t = "{ \"xs\": [ {\"n\":\"a\",\"v\":1}, {\"n\":\"b\",\"v\":2} ] }";
851 let (out, changed) = set(t, ".xs[n=b].v", "9");
852 assert!(changed);
853 assert_eq!(
854 out,
855 "{ \"xs\": [ {\"n\":\"a\",\"v\":1}, {\"n\":\"b\",\"v\":9} ] }"
856 );
857 let (del, _) = delete(t, ".xs[n=a]");
858 assert_eq!(del, "{ \"xs\": [ {\"n\":\"b\",\"v\":2} ] }");
859 }
860
861 #[test]
862 fn delete_takes_its_comma() {
863 assert_eq!(
864 delete("{\"a\":1,\"b\":2,\"c\":3}", ".b").0,
865 "{\"a\":1,\"c\":3}"
866 );
867 assert_eq!(delete("{\"a\":1,\"b\":2}", ".b").0, "{\"a\":1}");
868 assert_eq!(delete("{ \"a\": 1 }", ".a").0, "{}");
869 assert!(!delete("{\"a\":1}", ".z").1); }
871
872 #[test]
873 fn add_appends_without_index() {
874 let (out, changed) = add("{\"xs\": [1, 2]}", ".xs", "3");
875 assert!(changed);
876 assert_eq!(out, "{\"xs\": [1, 2, 3]}");
877 }
878
879 #[test]
880 fn move_reorders_preserving_separator() {
881 assert_eq!(
882 move_el("{\"xs\": [1, 2, 3]}", ".xs[0]", MoveTo::Last).0,
883 "{\"xs\": [2, 3, 1]}"
884 );
885 assert_eq!(
886 move_el("{\"xs\": [1, 2, 3]}", ".xs[2]", MoveTo::Up).0,
887 "{\"xs\": [1, 3, 2]}"
888 );
889 assert!(!move_el("{\"xs\": [1]}", ".xs[0]", MoveTo::Last).1); }
891
892 fn yaml_op_set(path: &str, value: &str) -> Op {
893 Op::Set {
894 path: parse_path(path).unwrap(),
895 raw: path.to_string(),
896 value: normalize_value(value),
897 }
898 }
899
900 #[test]
901 fn yaml_set_replace_and_delete_preserve_comments() {
902 let yaml = "# top\nserver:\n host: localhost # inline\n port: 8080\n debug: true\n";
903 let del = Op::Delete {
904 path: parse_path(".server.debug").unwrap(),
905 raw: ".server.debug".to_string(),
906 };
907 let (out, changes) = apply_yaml(yaml, &[yaml_op_set(".server.port", "9090"), del]).unwrap();
908 assert_eq!(changes, 2);
909 assert!(out.contains("# top"), "leading comment kept: {out:?}");
910 assert!(out.contains("port: 9090"), "number not quoted: {out:?}");
911 assert!(out.contains("# inline"), "inline comment kept: {out:?}");
912 assert!(!out.contains("debug:"), "debug deleted: {out:?}");
913 }
914
915 #[test]
916 fn yaml_add_and_predicate_paths_error() {
917 let add = Op::Add {
918 path: parse_path(".server.tags").unwrap(),
919 raw: ".server.tags".to_string(),
920 value: normalize_value("x"),
921 };
922 let e = apply_yaml("server:\n tags:\n - a\n", &[add]).unwrap_err();
923 assert!(e.contains("not yet supported for YAML"), "{e}");
924
925 let pred = apply_yaml("xs: []\n", &[yaml_op_set(".xs[n=a].v", "1")]).unwrap_err();
926 assert!(
927 pred.contains("predicate paths are not yet supported"),
928 "{pred}"
929 );
930 }
931}