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(
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 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 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 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 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 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}