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