Skip to main content

yaml_rt_core/
edit.rs

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