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#[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 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 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 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 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 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 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}