1use std::fmt;
2
3use crate::fragment::indent_text;
4use crate::pointer::parse_sequence_index;
5use crate::{
6 CollectionStyle, FragmentError, JsonPointer, NodeId, PointerError, ResolvedScalar,
7 SemanticKind, SemanticValueError, Span, YamlDoc, YamlError, YamlFragment, YamlScalarStyle,
8 resolve_scalar, semantically_equal,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct YamlEditError {
14 message: String,
15}
16
17impl YamlEditError {
18 fn new(message: impl Into<String>) -> Self {
19 Self {
20 message: message.into(),
21 }
22 }
23}
24
25impl fmt::Display for YamlEditError {
26 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27 formatter.write_str(&self.message)
28 }
29}
30
31impl std::error::Error for YamlEditError {}
32
33impl From<PointerError> for YamlEditError {
34 fn from(error: PointerError) -> Self {
35 Self::new(error.to_string())
36 }
37}
38
39impl From<FragmentError> for YamlEditError {
40 fn from(error: FragmentError) -> Self {
41 Self::new(error.to_string())
42 }
43}
44
45impl From<YamlError> for YamlEditError {
46 fn from(error: YamlError) -> Self {
47 Self::new(error.to_string())
48 }
49}
50
51impl From<SemanticValueError> for YamlEditError {
52 fn from(error: SemanticValueError) -> Self {
53 Self::new(error.to_string())
54 }
55}
56
57enum AddLocation {
58 Root(NodeId),
59 Mapping {
60 mapping: NodeId,
61 existing: Option<NodeId>,
62 key: String,
63 },
64 Sequence {
65 sequence: NodeId,
66 index: usize,
67 },
68}
69
70impl YamlDoc {
71 pub fn add_at(
73 &mut self,
74 document: usize,
75 pointer: &JsonPointer,
76 value: &YamlFragment,
77 ) -> Result<(), YamlEditError> {
78 self.transaction(|work| {
79 let location = work.resolve_add_location(document, pointer)?;
80 work.queue_add(location, value)
81 })
82 }
83
84 pub fn remove_at(
86 &mut self,
87 document: usize,
88 pointer: &JsonPointer,
89 ) -> Result<(), YamlEditError> {
90 if pointer.is_root() {
91 return Err(YamlEditError::new(
92 "removing a YAML document root is not supported",
93 ));
94 }
95 self.transaction(|work| {
96 let target = work.resolve_pointer(document, pointer)?;
97 work.queue_value_removal(target)
98 })
99 }
100
101 pub fn replace_at(
103 &mut self,
104 document: usize,
105 pointer: &JsonPointer,
106 value: &YamlFragment,
107 ) -> Result<(), YamlEditError> {
108 self.transaction(|work| {
109 let target = work.resolve_pointer(document, pointer)?;
110 work.queue_fragment_replacement(target, value)
111 })
112 }
113
114 pub fn move_at(
116 &mut self,
117 document: usize,
118 from: &JsonPointer,
119 path: &JsonPointer,
120 ) -> Result<(), YamlEditError> {
121 if from == path {
122 return Ok(());
123 }
124 if from.is_proper_prefix_of(path) {
125 return Err(YamlEditError::new(
126 "move source must not be a proper prefix of its destination",
127 ));
128 }
129 let mut work = self.clone();
130 let source = work.resolve_pointer(document, from)?;
131 if work.anchor_has_external_alias(source) {
132 return Err(YamlEditError::new(
133 "cannot move anchored subtree because an alias outside it depends on that anchor",
134 ));
135 }
136 let fragment = YamlFragment::from_document_node(&work, source)?;
137 if from.is_root() {
138 return Err(YamlEditError::new(
139 "a document root cannot be moved into another location",
140 ));
141 }
142 work.queue_value_removal(source)?;
143 work.commit_edits()?;
144 let location = work.resolve_add_location(document, path)?;
145 work.queue_add(location, &fragment)?;
146 work.commit_edits()?;
147 *self = work;
148 Ok(())
149 }
150
151 pub fn copy_at(
153 &mut self,
154 document: usize,
155 from: &JsonPointer,
156 path: &JsonPointer,
157 ) -> Result<(), YamlEditError> {
158 let source = self.resolve_pointer(document, from)?;
159 let fragment = YamlFragment::from_document_node(self, source)?;
160 if fragment.contains_anchor() {
161 return Err(YamlEditError::new(format!(
162 "cannot copy {:?}: subtree contains an anchor",
163 from.as_str()
164 )));
165 }
166 self.add_at(document, path, &fragment)
167 }
168
169 pub fn test_at(
171 &self,
172 document: usize,
173 pointer: &JsonPointer,
174 value: &YamlFragment,
175 ) -> Result<bool, YamlEditError> {
176 let target = self.resolve_pointer(document, pointer)?;
177 semantically_equal(self, target, value.document(), value.root()).map_err(Into::into)
178 }
179
180 fn transaction(
181 &mut self,
182 operation: impl FnOnce(&mut YamlDoc) -> Result<(), YamlEditError>,
183 ) -> Result<(), YamlEditError> {
184 let mut work = self.clone();
185 operation(&mut work)?;
186 work.commit_edits()?;
187 *self = work;
188 Ok(())
189 }
190
191 fn resolve_add_location(
192 &self,
193 document: usize,
194 pointer: &JsonPointer,
195 ) -> Result<AddLocation, YamlEditError> {
196 let Some((parent_pointer, token)) = pointer.parent() else {
197 let root = self
198 .document_root(document)?
199 .ok_or_else(|| YamlEditError::new("selected document has no root node"))?;
200 return Ok(AddLocation::Root(root));
201 };
202 let mut parent = self.resolve_pointer(document, &parent_pointer)?;
203 parent = self.resolve_aliases_for_pointer(
204 parent,
205 pointer,
206 pointer.tokens().len().saturating_sub(1),
207 )?;
208 match self.semantic_kind(parent) {
209 Some(SemanticKind::Mapping { .. }) => {
210 let existing = self
211 .mapping_match(
212 parent,
213 token,
214 pointer,
215 pointer.tokens().len().saturating_sub(1),
216 )?
217 .map(|entry| entry.value);
218 Ok(AddLocation::Mapping {
219 mapping: parent,
220 existing,
221 key: token.as_str().to_owned(),
222 })
223 }
224 Some(SemanticKind::Sequence { .. }) => {
225 let length = self.sequence_items(parent).count();
226 let parsed = parse_sequence_index(
227 token,
228 pointer,
229 pointer.tokens().len().saturating_sub(1),
230 true,
231 )?;
232 let index = if parsed == usize::MAX { length } else { parsed };
233 if index > length {
234 return Err(YamlEditError::new(format!(
235 "sequence index {index} is out of bounds for insertion into length {length}"
236 )));
237 }
238 Ok(AddLocation::Sequence {
239 sequence: parent,
240 index,
241 })
242 }
243 _ => Err(YamlEditError::new(format!(
244 "add parent {:?} is not a mapping or sequence",
245 parent_pointer.as_str()
246 ))),
247 }
248 }
249
250 fn queue_add(
251 &mut self,
252 location: AddLocation,
253 value: &YamlFragment,
254 ) -> Result<(), YamlEditError> {
255 match location {
256 AddLocation::Root(root) => self.queue_fragment_replacement(root, value),
257 AddLocation::Mapping {
258 existing: Some(existing),
259 ..
260 } => self.queue_fragment_replacement(existing, value),
261 AddLocation::Mapping {
262 mapping,
263 existing: None,
264 key,
265 } => self.queue_mapping_insert(mapping, &key, value),
266 AddLocation::Sequence { sequence, index } => {
267 self.queue_sequence_insert(sequence, index, value)
268 }
269 }
270 }
271
272 fn queue_fragment_replacement(
273 &mut self,
274 target: NodeId,
275 value: &YamlFragment,
276 ) -> Result<(), YamlEditError> {
277 if self
278 .node(target)
279 .is_some_and(|node| node.kind() == crate::NodeKind::Scalar)
280 && matches!(
281 value.document().semantic_kind(value.root()),
282 Some(SemanticKind::Scalar { .. })
283 )
284 && value.document().raw_tag(value.root()).is_none()
285 && value.document().anchor(value.root()).is_none()
286 {
287 let mut replacement = value.to_yaml()?;
288 if !replacement.contains(['\n', '\r']) {
289 let (span, target_style) = self.scalar_replacement_target(target)?;
290 if scalar_is_string(self, target)?
291 && scalar_is_string(value.document(), value.root())?
292 {
293 let decoded = value.document().scalar_value(value.root())?;
294 if let Ok(styled) = crate::format_scalar_value(&decoded, target_style) {
295 replacement = styled;
296 }
297 }
298 self.queue_edit(span, replacement)?;
299 return Ok(());
300 }
301 }
302 let replacement = if self.is_flow_context(target) {
303 value.render_flow(self)?
304 } else {
305 let yaml = value.prepared(self)?.to_yaml()?;
306 indent_continuation_lines(&yaml, self.node_indent(self.expect_node(target)?))
307 };
308 self.replace_node_text(target, replacement)?;
309 Ok(())
310 }
311
312 fn queue_value_removal(&mut self, target: NodeId) -> Result<(), YamlEditError> {
313 let Some(entry) = self.containing_entry(target) else {
314 self.remove_node(target)?;
315 return Ok(());
316 };
317 let Some(collection) = self.node(entry).and_then(|node| node.parent()) else {
318 self.remove_node(entry)?;
319 return Ok(());
320 };
321 let flow = matches!(
322 self.semantic_kind(collection),
323 Some(
324 SemanticKind::Mapping {
325 style: CollectionStyle::Flow
326 } | SemanticKind::Sequence {
327 style: CollectionStyle::Flow
328 }
329 )
330 );
331 if !flow {
332 self.remove_node(entry)?;
333 return Ok(());
334 }
335
336 let entries = self
337 .children(collection)
338 .filter(|node| self.containing_entry_child(*node))
339 .collect::<Vec<_>>();
340 let index = entries
341 .iter()
342 .position(|candidate| *candidate == entry)
343 .ok_or_else(|| YamlEditError::new("flow collection entry is missing"))?;
344 let entry_span = self.expect_node(entry)?.span;
345 let span = if let Some(next) = entries.get(index + 1).copied() {
346 Span::new(entry_span.start, self.expect_node(next)?.span.start)
347 } else if index > 0 {
348 let previous = self.expect_node(entries[index - 1])?.span;
349 Span::new(previous.end, entry_span.end)
350 } else {
351 entry_span
352 };
353 self.queue_edit(span, String::new())?;
354 Ok(())
355 }
356
357 fn containing_entry_child(&self, node: NodeId) -> bool {
358 self.node(node).is_some_and(|node| {
359 matches!(
360 node.kind(),
361 crate::NodeKind::MappingEntry | crate::NodeKind::SequenceEntry
362 )
363 })
364 }
365
366 fn queue_mapping_insert(
367 &mut self,
368 mapping: NodeId,
369 key: &str,
370 value: &YamlFragment,
371 ) -> Result<(), YamlEditError> {
372 let Some(SemanticKind::Mapping { style }) = self.semantic_kind(mapping) else {
373 return Err(YamlEditError::new(
374 "mapping insertion target is not a mapping",
375 ));
376 };
377 let key = emit_string_key(key);
378 match style {
379 CollectionStyle::Flow => {
380 let mapping_node = self.expect_node(mapping)?;
381 let close = closing_delimiter_offset(self, mapping_node.span, '}')?;
382 let prefix = if self.mapping_entries(mapping).next().is_some() {
383 ", "
384 } else {
385 ""
386 };
387 let value = value.render_flow(self)?;
388 self.queue_edit(
389 Span::empty_from_usize(close),
390 format!("{prefix}{key}: {value}"),
391 )?;
392 }
393 CollectionStyle::Block => {
394 let mapping_node = self.expect_node(mapping)?;
395 let indent = self.node_indent(mapping_node);
396 let offset = self.mapping_insertion_offset(mapping_node);
397 let mut insertion = insertion_prefix(self, offset);
398 let value = value.prepared(self)?.to_yaml()?;
399 insertion.push_str(&format_block_mapping_entry(
400 &key,
401 &value,
402 indent,
403 self.preferred_line_ending(),
404 ));
405 self.queue_edit(Span::empty_from_usize(offset), insertion)?;
406 }
407 }
408 Ok(())
409 }
410
411 fn queue_sequence_insert(
412 &mut self,
413 sequence: NodeId,
414 index: usize,
415 value: &YamlFragment,
416 ) -> Result<(), YamlEditError> {
417 let Some(SemanticKind::Sequence { style }) = self.semantic_kind(sequence) else {
418 return Err(YamlEditError::new(
419 "sequence insertion target is not a sequence",
420 ));
421 };
422 let items = self.sequence_items(sequence).collect::<Vec<_>>();
423 match style {
424 CollectionStyle::Flow => {
425 let sequence_node = self.expect_node(sequence)?;
426 let offset = if let Some(item) = items.get(index).copied() {
427 self.expect_node(item)?.span.start as usize
428 } else {
429 closing_delimiter_offset(self, sequence_node.span, ']')?
430 };
431 let value = value.render_flow(self)?;
432 let insertion = if items.is_empty() {
433 value
434 } else if index < items.len() {
435 format!("{value}, ")
436 } else {
437 format!(", {value}")
438 };
439 self.queue_edit(Span::empty_from_usize(offset), insertion)?;
440 }
441 CollectionStyle::Block => {
442 let sequence_node = self.expect_node(sequence)?;
443 let indent = self.node_indent(sequence_node);
444 let offset = if let Some(item) = items.get(index).copied() {
445 let entry = self.containing_entry(item).unwrap_or(item);
446 self.line_start_offset(self.expect_node(entry)?.span.start as usize)
447 } else {
448 self.sequence_insertion_offset(sequence_node)
449 };
450 let mut insertion = insertion_prefix(self, offset);
451 let value = value.prepared(self)?.to_yaml()?;
452 insertion.push_str(&format_block_sequence_entry(
453 &value,
454 indent,
455 self.preferred_line_ending(),
456 ));
457 self.queue_edit(Span::empty_from_usize(offset), insertion)?;
458 }
459 }
460 Ok(())
461 }
462
463 fn is_flow_context(&self, mut node: NodeId) -> bool {
464 while let Some(parent) = self.node(node).and_then(|node| node.parent()) {
465 if matches!(
466 self.semantic_kind(parent),
467 Some(
468 SemanticKind::Mapping {
469 style: CollectionStyle::Flow
470 } | SemanticKind::Sequence {
471 style: CollectionStyle::Flow
472 }
473 )
474 ) {
475 return true;
476 }
477 node = parent;
478 }
479 false
480 }
481
482 fn anchor_has_external_alias(&self, root: NodeId) -> bool {
483 let Some(root_span) = self.node(root).map(|node| node.span()) else {
484 return false;
485 };
486 let anchored = self
487 .nodes
488 .iter()
489 .enumerate()
490 .map(|(index, _)| NodeId::from_usize(index))
491 .filter(|node| {
492 self.anchor(*node).is_some()
493 && self.node(*node).is_some_and(|node| {
494 node.span().start >= root_span.start && node.span().end <= root_span.end
495 })
496 })
497 .collect::<Vec<_>>();
498 self.nodes
499 .iter()
500 .enumerate()
501 .map(|(index, _)| NodeId::from_usize(index))
502 .filter(|node| matches!(self.semantic_kind(*node), Some(SemanticKind::Alias)))
503 .any(|alias| {
504 let outside = self.node(alias).is_some_and(|node| {
505 node.span().start < root_span.start || node.span().end > root_span.end
506 });
507 outside
508 && self
509 .resolve_alias(alias)
510 .is_some_and(|target| anchored.contains(&target))
511 })
512 }
513
514 pub(crate) fn line_start_offset(&self, offset: usize) -> usize {
515 let offset = u32::try_from(offset).unwrap_or(u32::MAX);
516 match self.source.line_starts().binary_search(&offset) {
517 Ok(index) => self.source.line_starts()[index] as usize,
518 Err(index) => self.source.line_starts()[index.saturating_sub(1)] as usize,
519 }
520 }
521}
522
523fn closing_delimiter_offset(
524 doc: &YamlDoc,
525 span: Span,
526 delimiter: char,
527) -> Result<usize, YamlEditError> {
528 let source = doc.source.slice(span);
529 let relative = source
530 .rfind(delimiter)
531 .ok_or_else(|| YamlEditError::new(format!("missing `{delimiter}` delimiter")))?;
532 Ok(span.start as usize + relative)
533}
534
535fn insertion_prefix(doc: &YamlDoc, offset: usize) -> String {
536 if offset == doc.source.len()
537 && !doc
538 .source
539 .as_str()
540 .as_bytes()
541 .last()
542 .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
543 {
544 doc.preferred_line_ending().to_owned()
545 } else {
546 String::new()
547 }
548}
549
550fn format_block_mapping_entry(key: &str, value: &str, indent: usize, ending: &str) -> String {
551 let prefix = " ".repeat(indent);
552 if !value.contains(['\n', '\r']) {
553 return format!("{prefix}{key}: {value}{ending}");
554 }
555 let value = indent_text(value, indent + 2);
556 let mut output = format!("{prefix}{key}:{ending}{value}");
557 if !output.ends_with(['\n', '\r']) {
558 output.push_str(ending);
559 }
560 output
561}
562
563fn format_block_sequence_entry(value: &str, indent: usize, ending: &str) -> String {
564 let prefix = " ".repeat(indent);
565 if !value.contains(['\n', '\r']) {
566 return format!("{prefix}- {value}{ending}");
567 }
568 let value = indent_text(value, indent + 2);
569 let mut output = format!("{prefix}-{ending}{value}");
570 if !output.ends_with(['\n', '\r']) {
571 output.push_str(ending);
572 }
573 output
574}
575
576fn indent_continuation_lines(value: &str, indent: usize) -> String {
577 if indent == 0 {
578 return value.to_owned();
579 }
580 let prefix = " ".repeat(indent);
581 let mut output = String::with_capacity(value.len());
582 let mut after_break = false;
583 for character in value.chars() {
584 if after_break && !matches!(character, '\r' | '\n') {
585 output.push_str(&prefix);
586 after_break = false;
587 }
588 output.push(character);
589 if character == '\n' {
590 after_break = true;
591 } else if character != '\r' {
592 after_break = false;
593 }
594 }
595 output
596}
597
598fn emit_string_key(value: &str) -> String {
599 if safe_plain_string(value) {
600 value.to_owned()
601 } else {
602 crate::fragment::quote_string(value)
603 }
604}
605
606fn safe_plain_string(value: &str) -> bool {
607 if value.is_empty()
608 || value.trim() != value
609 || value.contains(['\n', '\r', '\t', ':', '#', '[', ']', '{', '}', ','])
610 || value.starts_with(['-', '?', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`'])
611 {
612 return false;
613 }
614 matches!(
615 resolve_scalar(value, YamlScalarStyle::Plain, None),
616 Ok(ResolvedScalar::String)
617 )
618}
619
620fn scalar_is_string(doc: &YamlDoc, node: NodeId) -> Result<bool, YamlEditError> {
621 let Some(SemanticKind::Scalar { style }) = doc.semantic_kind(node) else {
622 return Ok(false);
623 };
624 let value = doc.scalar_value(node)?;
625 let tag = doc.resolved_tag(node)?;
626 Ok(matches!(
627 resolve_scalar(&value, style, tag.as_deref()),
628 Ok(ResolvedScalar::String)
629 ))
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635
636 fn pointer(value: &str) -> JsonPointer {
637 JsonPointer::parse(value).unwrap()
638 }
639
640 fn fragment(value: &str) -> YamlFragment {
641 YamlFragment::parse(value).unwrap()
642 }
643
644 #[test]
645 fn adds_replaces_and_removes_block_mapping_values() {
646 let mut doc = YamlDoc::parse("server:\n host: localhost # keep\n").unwrap();
647 doc.add_at(0, &pointer("/server/port"), &fragment("8080"))
648 .unwrap();
649 assert_eq!(
650 doc.as_source(),
651 "server:\n host: localhost # keep\n port: 8080\n"
652 );
653 doc.replace_at(0, &pointer("/server/host"), &fragment("example.com"))
654 .unwrap();
655 assert_eq!(
656 doc.as_source(),
657 "server:\n host: example.com # keep\n port: 8080\n"
658 );
659 doc.remove_at(0, &pointer("/server/port")).unwrap();
660 assert_eq!(doc.as_source(), "server:\n host: example.com # keep\n");
661 }
662
663 #[test]
664 fn adds_string_keys_without_changing_their_schema_type() {
665 let mut doc = YamlDoc::parse("{}\n").unwrap();
666 doc.add_at(0, &pointer("/true"), &fragment("value"))
667 .unwrap();
668 assert_eq!(doc.as_source(), "{\"true\": value}\n");
669 assert!(
670 doc.resolve_pointer(0, &pointer("/true")).is_ok(),
671 "{}",
672 doc.as_source()
673 );
674 }
675
676 #[test]
677 fn inserts_block_and_flow_sequence_items() {
678 let mut block = YamlDoc::parse("items:\n - a\n - c\n").unwrap();
679 block
680 .add_at(0, &pointer("/items/1"), &fragment("b"))
681 .unwrap();
682 block
683 .add_at(0, &pointer("/items/-"), &fragment("d"))
684 .unwrap();
685 assert_eq!(block.as_source(), "items:\n - a\n - b\n - c\n - d\n");
686
687 let mut flow = YamlDoc::parse("items: [a, c]\n").unwrap();
688 flow.add_at(0, &pointer("/items/1"), &fragment("b"))
689 .unwrap();
690 assert_eq!(flow.as_source(), "items: [a, b, c]\n");
691 }
692
693 #[test]
694 fn mutations_are_transactional() {
695 let input = "items: [a]\n";
696 let mut doc = YamlDoc::parse(input).unwrap();
697 assert!(doc.add_at(0, &pointer("/items/4"), &fragment("x")).is_err());
698 assert_eq!(doc.as_source(), input);
699 }
700
701 #[test]
702 fn move_uses_remove_then_add_sequence_indices() {
703 let mut doc = YamlDoc::parse("[a, b, c]\n").unwrap();
704 doc.move_at(0, &pointer("/0"), &pointer("/2")).unwrap();
705 assert_eq!(doc.as_source(), "[b, c, a]\n");
706 }
707
708 #[test]
709 fn copy_rejects_anchors_and_test_is_semantic() {
710 let mut doc = YamlDoc::parse("one: &one {value: 1}\ntwo: null\n").unwrap();
711 assert!(
712 doc.copy_at(0, &pointer("/one"), &pointer("/two"))
713 .unwrap_err()
714 .to_string()
715 .contains("anchor")
716 );
717 assert!(
718 doc.test_at(0, &pointer("/one/value"), &fragment("1.0"))
719 .unwrap()
720 );
721 }
722}