1use std::borrow::Cow;
10use std::sync::Arc;
11
12use crate::event::{CollectionStyle, ScalarStyle};
13use crate::pos::{LineIndex, Span};
14
15#[derive(Debug, Clone)]
21pub struct Document<Loc = Span> {
22 pub root: Node<Loc>,
24 pub version: Option<(u8, u8)>,
26 pub tags: Vec<(String, String)>,
28 pub comments: Vec<String>,
30 pub explicit_start: bool,
32 pub explicit_end: bool,
34 pub(crate) line_index: Option<Arc<LineIndex>>,
39}
40
41impl<Loc: PartialEq> PartialEq for Document<Loc> {
42 fn eq(&self, other: &Self) -> bool {
43 self.root == other.root
44 && self.version == other.version
45 && self.tags == other.tags
46 && self.comments == other.comments
47 && self.explicit_start == other.explicit_start
48 && self.explicit_end == other.explicit_end
49 }
52}
53
54impl<Loc> Document<Loc> {
55 pub const fn with_root(root: Node<Loc>) -> Self {
62 Self {
63 root,
64 version: None,
65 tags: Vec::new(),
66 comments: Vec::new(),
67 explicit_start: false,
68 explicit_end: false,
69 line_index: None,
70 }
71 }
72}
73
74impl Document<Span> {
75 #[must_use]
84 #[expect(
85 clippy::expect_used,
86 reason = "documents from load() always have line_index set; None only for manually constructed test documents"
87 )]
88 pub fn line_index(&self) -> &LineIndex {
89 self.line_index
90 .as_deref()
91 .expect("Document<Span> must have a LineIndex from the loader")
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct NodeMeta<Loc = Span> {
104 pub anchor: Option<String>,
106 pub anchor_loc: Option<Loc>,
109 pub tag_loc: Option<Loc>,
112 pub leading_comments: Option<Vec<String>>,
117 pub trailing_comment: Option<String>,
119}
120
121impl<Loc> NodeMeta<Loc> {
122 #[inline]
125 pub(crate) const fn is_all_none(&self) -> bool {
126 self.anchor.is_none()
127 && self.anchor_loc.is_none()
128 && self.tag_loc.is_none()
129 && self.leading_comments.is_none()
130 && self.trailing_comment.is_none()
131 }
132
133 #[inline]
135 pub fn into_option(self) -> Option<Box<Self>> {
136 if self.is_all_none() {
137 None
138 } else {
139 Some(Box::new(self))
140 }
141 }
142}
143
144#[derive(Debug, Clone, PartialEq)]
146pub enum Node<Loc = Span> {
147 Scalar {
149 value: String,
151 style: ScalarStyle,
153 tag: Option<Cow<'static, str>>,
155 loc: Loc,
157 meta: Option<Box<NodeMeta<Loc>>>,
160 },
161 Mapping {
163 entries: Vec<(Self, Self)>,
165 style: CollectionStyle,
167 tag: Option<Cow<'static, str>>,
169 loc: Loc,
171 meta: Option<Box<NodeMeta<Loc>>>,
174 },
175 Sequence {
177 items: Vec<Self>,
179 style: CollectionStyle,
181 tag: Option<Cow<'static, str>>,
183 loc: Loc,
185 meta: Option<Box<NodeMeta<Loc>>>,
188 },
189 Alias {
191 name: String,
193 loc: Loc,
195 leading_comments: Option<Vec<String>>,
197 trailing_comment: Option<String>,
199 },
200}
201
202impl<Loc> Node<Loc> {
203 #[inline]
205 pub fn anchor(&self) -> Option<&str> {
206 match self {
207 Self::Scalar { meta, .. }
208 | Self::Mapping { meta, .. }
209 | Self::Sequence { meta, .. } => meta.as_ref().and_then(|m| m.anchor.as_deref()),
210 Self::Alias { .. } => None,
211 }
212 }
213
214 #[inline]
219 pub fn anchor_loc(&self) -> Option<Loc>
220 where
221 Loc: Copy,
222 {
223 match self {
224 Self::Scalar { meta, .. }
225 | Self::Mapping { meta, .. }
226 | Self::Sequence { meta, .. } => meta.as_ref().and_then(|m| m.anchor_loc),
227 Self::Alias { .. } => None,
228 }
229 }
230
231 #[inline]
236 pub fn tag_loc(&self) -> Option<Loc>
237 where
238 Loc: Copy,
239 {
240 match self {
241 Self::Scalar { meta, .. }
242 | Self::Mapping { meta, .. }
243 | Self::Sequence { meta, .. } => meta.as_ref().and_then(|m| m.tag_loc),
244 Self::Alias { .. } => None,
245 }
246 }
247
248 #[inline]
250 pub fn leading_comments(&self) -> &[String] {
251 match self {
252 Self::Scalar { meta, .. }
253 | Self::Mapping { meta, .. }
254 | Self::Sequence { meta, .. } => meta
255 .as_ref()
256 .and_then(|m| m.leading_comments.as_deref())
257 .unwrap_or(&[]),
258 Self::Alias {
259 leading_comments, ..
260 } => leading_comments.as_deref().unwrap_or(&[]),
261 }
262 }
263
264 #[inline]
266 pub fn trailing_comment(&self) -> Option<&str> {
267 match self {
268 Self::Scalar { meta, .. }
269 | Self::Mapping { meta, .. }
270 | Self::Sequence { meta, .. } => {
271 meta.as_ref().and_then(|m| m.trailing_comment.as_deref())
272 }
273 Self::Alias {
274 trailing_comment, ..
275 } => trailing_comment.as_deref(),
276 }
277 }
278
279 pub fn clear_anchor(&mut self) {
282 match self {
283 Self::Scalar { meta, .. }
284 | Self::Mapping { meta, .. }
285 | Self::Sequence { meta, .. } => {
286 if let Some(m) = meta.as_mut() {
287 m.anchor = None;
288 m.anchor_loc = None;
289 if m.is_all_none() {
291 *meta = None;
292 }
293 }
294 }
295 Self::Alias { .. } => {}
296 }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use std::borrow::Cow;
303
304 use super::*;
305 use crate::event::{CollectionStyle, ScalarStyle};
306 use crate::pos::Span;
307
308 fn zero_span() -> Span {
309 Span { start: 0, end: 0 }
310 }
311
312 fn plain_scalar(value: &str) -> Node<Span> {
313 Node::Scalar {
314 value: value.to_owned(),
315 style: ScalarStyle::Plain,
316 tag: None,
317 loc: zero_span(),
318 meta: None,
319 }
320 }
321
322 #[test]
328 fn scalar_all_none_meta_fields_produces_meta_none() {
329 let node = Node::Scalar {
330 value: "v".to_owned(),
331 style: ScalarStyle::Plain,
332 tag: None,
333 loc: zero_span(),
334 meta: NodeMeta {
335 anchor: None,
336 anchor_loc: None,
337 tag_loc: None,
338 leading_comments: None,
339 trailing_comment: None,
340 }
341 .into_option(),
342 };
343 assert!(
344 matches!(node, Node::Scalar { meta: None, .. }),
345 "all-None meta fields must produce meta: None"
346 );
347 }
348
349 #[test]
351 fn scalar_with_anchor_only_produces_meta_some() {
352 let node = Node::Scalar {
353 value: "v".to_owned(),
354 style: ScalarStyle::Plain,
355 tag: None,
356 loc: zero_span(),
357 meta: NodeMeta {
358 anchor: Some("a".to_owned()),
359 anchor_loc: None,
360 tag_loc: None,
361 leading_comments: None,
362 trailing_comment: None,
363 }
364 .into_option(),
365 };
366 assert!(
367 matches!(node, Node::Scalar { meta: Some(_), .. }),
368 "anchor-only meta must produce meta: Some"
369 );
370 }
371
372 #[test]
374 fn scalar_with_leading_comment_only_produces_meta_some() {
375 let node = Node::Scalar {
376 value: "v".to_owned(),
377 style: ScalarStyle::Plain,
378 tag: None,
379 loc: zero_span(),
380 meta: NodeMeta {
381 anchor: None,
382 anchor_loc: None,
383 tag_loc: None,
384 leading_comments: Some(vec!["# x".to_owned()]),
385 trailing_comment: None,
386 }
387 .into_option(),
388 };
389 assert!(
390 matches!(node, Node::Scalar { meta: Some(_), .. }),
391 "leading-comment-only meta must produce meta: Some"
392 );
393 }
394
395 #[test]
397 fn scalar_with_trailing_comment_only_produces_meta_some() {
398 let node = Node::Scalar {
399 value: "v".to_owned(),
400 style: ScalarStyle::Plain,
401 tag: None,
402 loc: zero_span(),
403 meta: NodeMeta {
404 anchor: None,
405 anchor_loc: None,
406 tag_loc: None,
407 leading_comments: None,
408 trailing_comment: Some("# y".to_owned()),
409 }
410 .into_option(),
411 };
412 assert!(
413 matches!(node, Node::Scalar { meta: Some(_), .. }),
414 "trailing-comment-only meta must produce meta: Some"
415 );
416 }
417
418 #[test]
420 fn scalar_with_tag_loc_only_produces_meta_some() {
421 let node = Node::Scalar {
422 value: "v".to_owned(),
423 style: ScalarStyle::Plain,
424 tag: None,
425 loc: zero_span(),
426 meta: NodeMeta {
427 anchor: None,
428 anchor_loc: None,
429 tag_loc: Some(zero_span()),
430 leading_comments: None,
431 trailing_comment: None,
432 }
433 .into_option(),
434 };
435 assert!(
436 matches!(node, Node::Scalar { meta: Some(_), .. }),
437 "tag-loc-only meta must produce meta: Some"
438 );
439 }
440
441 #[test]
443 fn mapping_all_none_meta_fields_produces_meta_none() {
444 let node = Node::Mapping {
445 entries: vec![],
446 style: CollectionStyle::Block,
447 tag: None,
448 loc: zero_span(),
449 meta: NodeMeta {
450 anchor: None,
451 anchor_loc: None,
452 tag_loc: None,
453 leading_comments: None,
454 trailing_comment: None,
455 }
456 .into_option(),
457 };
458 assert!(
459 matches!(node, Node::Mapping { meta: None, .. }),
460 "all-None mapping meta must produce meta: None"
461 );
462 }
463
464 #[test]
466 fn sequence_all_none_meta_fields_produces_meta_none() {
467 let node = Node::Sequence {
468 items: vec![],
469 style: CollectionStyle::Block,
470 tag: None,
471 loc: zero_span(),
472 meta: NodeMeta {
473 anchor: None,
474 anchor_loc: None,
475 tag_loc: None,
476 leading_comments: None,
477 trailing_comment: None,
478 }
479 .into_option(),
480 };
481 assert!(
482 matches!(node, Node::Sequence { meta: None, .. }),
483 "all-None sequence meta must produce meta: None"
484 );
485 }
486
487 #[test]
493 fn accessor_anchor_returns_none_when_meta_is_none() {
494 let node = plain_scalar("v");
495 assert_eq!(node.anchor(), None);
496 }
497
498 #[test]
500 fn accessor_anchor_returns_some_when_meta_is_some() {
501 let node = Node::Scalar {
502 value: "v".to_owned(),
503 style: ScalarStyle::Plain,
504 tag: None,
505 loc: zero_span(),
506 meta: NodeMeta {
507 anchor: Some("a".to_owned()),
508 anchor_loc: None,
509 tag_loc: None,
510 leading_comments: None,
511 trailing_comment: None,
512 }
513 .into_option(),
514 };
515 assert_eq!(node.anchor(), Some("a"));
516 }
517
518 #[test]
520 fn accessor_anchor_loc_returns_none_when_meta_is_none() {
521 let node = plain_scalar("v");
522 assert_eq!(node.anchor_loc(), None);
523 }
524
525 #[test]
527 fn accessor_anchor_loc_returns_some_when_set() {
528 let span = zero_span();
529 let node = Node::Scalar {
530 value: "v".to_owned(),
531 style: ScalarStyle::Plain,
532 tag: None,
533 loc: zero_span(),
534 meta: NodeMeta {
535 anchor: Some("a".to_owned()),
536 anchor_loc: Some(span),
537 tag_loc: None,
538 leading_comments: None,
539 trailing_comment: None,
540 }
541 .into_option(),
542 };
543 assert_eq!(node.anchor_loc(), Some(span));
544 }
545
546 #[test]
548 fn accessor_tag_loc_returns_none_when_meta_is_none() {
549 let node = plain_scalar("v");
550 assert_eq!(node.tag_loc(), None);
551 }
552
553 #[test]
555 fn accessor_tag_loc_returns_some_when_set() {
556 let span = zero_span();
557 let node = Node::Scalar {
558 value: "v".to_owned(),
559 style: ScalarStyle::Plain,
560 tag: None,
561 loc: zero_span(),
562 meta: NodeMeta {
563 anchor: None,
564 anchor_loc: None,
565 tag_loc: Some(span),
566 leading_comments: None,
567 trailing_comment: None,
568 }
569 .into_option(),
570 };
571 assert_eq!(node.tag_loc(), Some(span));
572 }
573
574 #[test]
576 fn accessor_leading_comments_returns_empty_slice_when_meta_is_none() {
577 let node = plain_scalar("v");
578 assert_eq!(node.leading_comments(), &[] as &[String]);
579 }
580
581 #[test]
583 fn accessor_leading_comments_returns_slice_when_set() {
584 let node = Node::Scalar {
585 value: "v".to_owned(),
586 style: ScalarStyle::Plain,
587 tag: None,
588 loc: zero_span(),
589 meta: NodeMeta {
590 anchor: None,
591 anchor_loc: None,
592 tag_loc: None,
593 leading_comments: Some(vec!["# x".to_owned()]),
594 trailing_comment: None,
595 }
596 .into_option(),
597 };
598 assert_eq!(node.leading_comments(), &["# x"]);
599 }
600
601 #[test]
603 fn accessor_trailing_comment_returns_none_when_meta_is_none() {
604 let node = plain_scalar("v");
605 assert_eq!(node.trailing_comment(), None);
606 }
607
608 #[test]
610 fn accessor_trailing_comment_returns_some_when_set() {
611 let node = Node::Scalar {
612 value: "v".to_owned(),
613 style: ScalarStyle::Plain,
614 tag: None,
615 loc: zero_span(),
616 meta: NodeMeta {
617 anchor: None,
618 anchor_loc: None,
619 tag_loc: None,
620 leading_comments: None,
621 trailing_comment: Some("# y".to_owned()),
622 }
623 .into_option(),
624 };
625 assert_eq!(node.trailing_comment(), Some("# y"));
626 }
627
628 #[test]
630 fn alias_anchor_returns_none() {
631 let node = Node::Alias {
632 name: "x".to_owned(),
633 loc: zero_span(),
634 leading_comments: None,
635 trailing_comment: None,
636 };
637 assert_eq!(node.anchor(), None);
638 assert_eq!(node.anchor_loc(), None);
639 assert_eq!(node.tag_loc(), None);
640 }
641
642 const _: () = assert!(
644 std::mem::size_of::<Node<Span>>() <= 120,
645 "Node<Span> must be <= 120 bytes"
646 );
647 #[test]
648 fn node_span_size_fits_target() {
649 let size = std::mem::size_of::<Node<Span>>();
650 assert!(
651 size <= 120,
652 "Node<Span> size {size} exceeds 120-byte target"
653 );
654 }
655
656 #[test]
658 fn clear_anchor_sets_anchor_and_anchor_loc_to_none() {
659 let mut node = Node::Scalar {
660 value: "v".to_owned(),
661 style: ScalarStyle::Plain,
662 tag: None,
663 loc: zero_span(),
664 meta: NodeMeta {
665 anchor: Some("a".to_owned()),
666 anchor_loc: Some(zero_span()),
667 tag_loc: None,
668 leading_comments: None,
669 trailing_comment: None,
670 }
671 .into_option(),
672 };
673 node.clear_anchor();
674 assert_eq!(
675 node.anchor(),
676 None,
677 "anchor must be None after clear_anchor"
678 );
679 assert_eq!(
680 node.anchor_loc(),
681 None,
682 "anchor_loc must be None after clear_anchor"
683 );
684 assert!(
686 matches!(node, Node::Scalar { meta: None, .. }),
687 "meta must collapse to None when all fields become None"
688 );
689 }
690
691 #[test]
693 fn node_debug_includes_leading_comments() {
694 let node = Node::Scalar {
695 value: "val".to_owned(),
696 style: ScalarStyle::Plain,
697 tag: None,
698 loc: zero_span(),
699 meta: NodeMeta {
700 anchor: None,
701 anchor_loc: None,
702 tag_loc: None,
703 leading_comments: Some(vec!["# note".to_owned()]),
704 trailing_comment: None,
705 }
706 .into_option(),
707 };
708 let debug = format!("{node:?}");
709 assert!(debug.contains("# note"), "debug output: {debug}");
710 }
711
712 #[test]
714 fn node_partial_eq_considers_leading_comments() {
715 let a = Node::Scalar {
716 value: "val".to_owned(),
717 style: ScalarStyle::Plain,
718 tag: None,
719 loc: zero_span(),
720 meta: NodeMeta {
721 anchor: None,
722 anchor_loc: None,
723 tag_loc: None,
724 leading_comments: Some(vec!["# a".to_owned()]),
725 trailing_comment: None,
726 }
727 .into_option(),
728 };
729 let b = Node::Scalar {
730 value: "val".to_owned(),
731 style: ScalarStyle::Plain,
732 tag: None,
733 loc: zero_span(),
734 meta: NodeMeta {
735 anchor: None,
736 anchor_loc: None,
737 tag_loc: None,
738 leading_comments: Some(vec!["# b".to_owned()]),
739 trailing_comment: None,
740 }
741 .into_option(),
742 };
743 assert_ne!(a, b);
744 }
745
746 #[test]
748 fn node_clone_preserves_comments() {
749 let node = Node::Scalar {
750 value: "val".to_owned(),
751 style: ScalarStyle::Plain,
752 tag: None,
753 loc: zero_span(),
754 meta: NodeMeta {
755 anchor: None,
756 anchor_loc: None,
757 tag_loc: None,
758 leading_comments: Some(vec!["# x".to_owned()]),
759 trailing_comment: Some("# y".to_owned()),
760 }
761 .into_option(),
762 };
763 let cloned = node.clone();
764 assert_eq!(node, cloned);
765 assert_eq!(cloned.leading_comments(), &["# x"]);
766 assert_eq!(cloned.trailing_comment(), Some("# y"));
767 }
768
769 #[test]
771 fn plain_scalar_has_empty_comments() {
772 let n = plain_scalar("hello");
773 assert!(n.leading_comments().is_empty());
774 assert!(n.trailing_comment().is_none());
775 }
776
777 fn bare_document(explicit_start: bool, explicit_end: bool) -> Document<Span> {
778 Document {
779 root: plain_scalar("val"),
780 version: None,
781 tags: Vec::new(),
782 comments: Vec::new(),
783 explicit_start,
784 explicit_end,
785 line_index: None,
786 }
787 }
788
789 #[test]
791 fn document_explicit_flags_in_equality() {
792 let a = bare_document(false, false);
793 let b = bare_document(false, false);
794 assert_eq!(a, b);
795 }
796
797 #[test]
799 fn document_partial_eq_distinguishes_explicit_start() {
800 let a = bare_document(true, false);
801 let b = bare_document(false, false);
802 assert_ne!(a, b);
803 }
804
805 #[test]
807 fn document_partial_eq_distinguishes_explicit_end() {
808 let a = bare_document(false, true);
809 let b = bare_document(false, false);
810 assert_ne!(a, b);
811 }
812
813 #[test]
815 fn document_clone_preserves_explicit_flags() {
816 let doc = bare_document(true, true);
817 let cloned = doc.clone();
818 assert_eq!(doc, cloned);
819 assert!(cloned.explicit_start);
820 assert!(cloned.explicit_end);
821 }
822
823 #[test]
829 fn anchor_loc_accessor_returns_some_for_anchored_scalar() {
830 let span = zero_span();
831 let node = Node::Scalar {
832 value: "v".to_owned(),
833 style: ScalarStyle::Plain,
834 tag: None,
835 loc: zero_span(),
836 meta: NodeMeta {
837 anchor: Some("a".to_owned()),
838 anchor_loc: Some(span),
839 tag_loc: None,
840 leading_comments: None,
841 trailing_comment: None,
842 }
843 .into_option(),
844 };
845 assert_eq!(node.anchor_loc(), Some(span));
846 }
847
848 #[test]
850 fn anchor_loc_accessor_returns_none_for_unanchored_scalar() {
851 let node = plain_scalar("v");
852 assert_eq!(node.anchor_loc(), None);
853 }
854
855 #[test]
857 fn anchor_loc_accessor_returns_none_for_alias() {
858 let node = Node::Alias {
859 name: "x".to_owned(),
860 loc: zero_span(),
861 leading_comments: None,
862 trailing_comment: None,
863 };
864 assert_eq!(node.anchor_loc(), None);
865 }
866
867 #[test]
869 fn anchor_loc_accessor_returns_some_for_anchored_mapping() {
870 let span = zero_span();
871 let node = Node::Mapping {
872 entries: vec![],
873 style: CollectionStyle::Block,
874 tag: None,
875 loc: zero_span(),
876 meta: NodeMeta {
877 anchor: Some("m".to_owned()),
878 anchor_loc: Some(span),
879 tag_loc: None,
880 leading_comments: None,
881 trailing_comment: None,
882 }
883 .into_option(),
884 };
885 assert_eq!(node.anchor_loc(), Some(span));
886 }
887
888 #[test]
890 fn anchor_loc_accessor_returns_some_for_anchored_sequence() {
891 let span = zero_span();
892 let node = Node::Sequence {
893 items: vec![],
894 style: CollectionStyle::Block,
895 tag: None,
896 loc: zero_span(),
897 meta: NodeMeta {
898 anchor: Some("s".to_owned()),
899 anchor_loc: Some(span),
900 tag_loc: None,
901 leading_comments: None,
902 trailing_comment: None,
903 }
904 .into_option(),
905 };
906 assert_eq!(node.anchor_loc(), Some(span));
907 }
908
909 #[test]
915 fn tag_loc_accessor_returns_some_for_tagged_scalar() {
916 let span = zero_span();
917 let node = Node::Scalar {
918 value: "v".to_owned(),
919 style: ScalarStyle::Plain,
920 tag: Some(Cow::Owned("!t".to_owned())),
921 loc: zero_span(),
922 meta: NodeMeta {
923 anchor: None,
924 anchor_loc: None,
925 tag_loc: Some(span),
926 leading_comments: None,
927 trailing_comment: None,
928 }
929 .into_option(),
930 };
931 assert_eq!(node.tag_loc(), Some(span));
932 }
933
934 #[test]
936 fn tag_loc_accessor_returns_none_for_untagged_scalar() {
937 let node = plain_scalar("v");
938 assert_eq!(node.tag_loc(), None);
939 }
940
941 #[test]
943 fn tag_loc_accessor_returns_none_for_alias() {
944 let node = Node::Alias {
945 name: "x".to_owned(),
946 loc: zero_span(),
947 leading_comments: None,
948 trailing_comment: None,
949 };
950 assert_eq!(node.tag_loc(), None);
951 }
952
953 #[test]
955 fn tag_loc_accessor_returns_some_for_tagged_mapping() {
956 let span = zero_span();
957 let node = Node::Mapping {
958 entries: vec![],
959 style: CollectionStyle::Block,
960 tag: Some(Cow::Owned("!!map".to_owned())),
961 loc: zero_span(),
962 meta: NodeMeta {
963 anchor: None,
964 anchor_loc: None,
965 tag_loc: Some(span),
966 leading_comments: None,
967 trailing_comment: None,
968 }
969 .into_option(),
970 };
971 assert_eq!(node.tag_loc(), Some(span));
972 }
973
974 #[test]
976 fn tag_loc_accessor_returns_some_for_tagged_sequence() {
977 let span = zero_span();
978 let node = Node::Sequence {
979 items: vec![],
980 style: CollectionStyle::Block,
981 tag: Some(Cow::Owned("!!seq".to_owned())),
982 loc: zero_span(),
983 meta: NodeMeta {
984 anchor: None,
985 anchor_loc: None,
986 tag_loc: Some(span),
987 leading_comments: None,
988 trailing_comment: None,
989 }
990 .into_option(),
991 };
992 assert_eq!(node.tag_loc(), Some(span));
993 }
994
995 #[test]
1001 fn node_construction_with_borrowed_tag() {
1002 let node = Node::Scalar {
1003 value: "v".to_owned(),
1004 style: ScalarStyle::Plain,
1005 tag: Some(Cow::Borrowed("tag:yaml.org,2002:str")),
1006 loc: zero_span(),
1007 meta: None,
1008 };
1009 assert_eq!(node.tag_loc(), None);
1010 if let Node::Scalar { tag, .. } = &node {
1011 assert!(matches!(tag, Some(Cow::Borrowed(_))));
1012 }
1013 }
1014
1015 #[test]
1017 fn node_construction_with_owned_tag() {
1018 let node = Node::Scalar {
1019 value: "v".to_owned(),
1020 style: ScalarStyle::Plain,
1021 tag: Some(Cow::Owned("!custom".to_owned())),
1022 loc: zero_span(),
1023 meta: None,
1024 };
1025 assert_eq!(node.tag_loc(), None);
1026 if let Node::Scalar { tag, .. } = &node {
1027 assert_eq!(tag.as_deref(), Some("!custom"));
1028 }
1029 }
1030
1031 #[test]
1033 fn node_partial_eq_borrowed_vs_owned_same_content() {
1034 let borrowed = Node::Scalar {
1035 value: "v".to_owned(),
1036 style: ScalarStyle::Plain,
1037 tag: Some(Cow::Borrowed("tag:yaml.org,2002:str")),
1038 loc: zero_span(),
1039 meta: None,
1040 };
1041 let owned = Node::Scalar {
1042 value: "v".to_owned(),
1043 style: ScalarStyle::Plain,
1044 tag: Some(Cow::Owned("tag:yaml.org,2002:str".to_owned())),
1045 loc: zero_span(),
1046 meta: None,
1047 };
1048 assert_eq!(borrowed, owned);
1049 }
1050
1051 #[test]
1053 fn node_clone_preserves_cow_variant() {
1054 let node = Node::Scalar {
1055 value: "v".to_owned(),
1056 style: ScalarStyle::Plain,
1057 tag: Some(Cow::Borrowed("tag:yaml.org,2002:str")),
1058 loc: zero_span(),
1059 meta: None,
1060 };
1061 let cloned_tag = if let Node::Scalar { tag, .. } = &node {
1062 tag.clone()
1063 } else {
1064 unreachable!()
1065 };
1066 assert!(
1067 matches!(cloned_tag, Some(Cow::Borrowed(_))),
1068 "cloned Borrowed tag must remain Borrowed"
1069 );
1070 }
1071}