1use std::collections::{HashMap, HashSet};
24
25use crate::event::{Event, ScalarStyle};
26use crate::node::{Document, Node};
27use crate::pos::{Pos, Span};
28
29#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
35pub enum LoadError {
36 #[error("parse error at {pos:?}: {message}")]
38 Parse { pos: Pos, message: String },
39
40 #[error("nesting depth limit exceeded (max: {limit})")]
42 NestingDepthLimitExceeded { limit: usize },
43
44 #[error("anchor count limit exceeded (max: {limit})")]
46 AnchorCountLimitExceeded { limit: usize },
47
48 #[error("alias expansion node limit exceeded (max: {limit})")]
50 AliasExpansionLimitExceeded { limit: usize },
51
52 #[error("circular alias reference: '{name}'")]
54 CircularAlias { name: String },
55
56 #[error("undefined alias: '{name}'")]
58 UndefinedAlias { name: String },
59}
60
61type Result<T> = std::result::Result<T, LoadError>;
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum LoadMode {
71 Lossless,
73 Resolved,
75}
76
77#[derive(Debug, Clone)]
79pub struct LoaderOptions {
80 pub max_nesting_depth: usize,
82 pub max_anchors: usize,
84 pub max_expanded_nodes: usize,
87 pub mode: LoadMode,
89}
90
91impl Default for LoaderOptions {
92 fn default() -> Self {
93 Self {
94 max_nesting_depth: 512,
95 max_anchors: 10_000,
96 max_expanded_nodes: 1_000_000,
97 mode: LoadMode::Lossless,
98 }
99 }
100}
101
102pub struct LoaderBuilder {
115 options: LoaderOptions,
116}
117
118impl LoaderBuilder {
119 #[must_use]
121 pub fn new() -> Self {
122 Self {
123 options: LoaderOptions::default(),
124 }
125 }
126
127 #[must_use]
129 pub const fn lossless(mut self) -> Self {
130 self.options.mode = LoadMode::Lossless;
131 self
132 }
133
134 #[must_use]
136 pub const fn resolved(mut self) -> Self {
137 self.options.mode = LoadMode::Resolved;
138 self
139 }
140
141 #[must_use]
143 pub const fn max_nesting_depth(mut self, limit: usize) -> Self {
144 self.options.max_nesting_depth = limit;
145 self
146 }
147
148 #[must_use]
150 pub const fn max_anchors(mut self, limit: usize) -> Self {
151 self.options.max_anchors = limit;
152 self
153 }
154
155 #[must_use]
157 pub const fn max_expanded_nodes(mut self, limit: usize) -> Self {
158 self.options.max_expanded_nodes = limit;
159 self
160 }
161
162 #[must_use]
164 pub const fn build(self) -> Loader {
165 Loader {
166 options: self.options,
167 }
168 }
169}
170
171impl Default for LoaderBuilder {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177pub struct Loader {
183 options: LoaderOptions,
184}
185
186impl Loader {
187 pub fn load(&self, input: &str) -> std::result::Result<Vec<Document<Span>>, LoadError> {
194 let mut state = LoadState::new(&self.options);
195 state.run(input)
196 }
197}
198
199pub fn load(input: &str) -> std::result::Result<Vec<Document<Span>>, LoadError> {
219 LoaderBuilder::new().lossless().build().load(input)
220}
221
222struct LoadState<'opt> {
227 options: &'opt LoaderOptions,
228 anchor_map: HashMap<String, Node<Span>>,
230 anchor_count: usize,
232 depth: usize,
234 expanded_nodes: usize,
236}
237
238impl<'opt> LoadState<'opt> {
239 fn new(options: &'opt LoaderOptions) -> Self {
240 Self {
241 options,
242 anchor_map: HashMap::new(),
243 anchor_count: 0,
244 depth: 0,
245 expanded_nodes: 0,
246 }
247 }
248
249 fn reset_for_document(&mut self) {
250 self.anchor_map.clear();
251 self.anchor_count = 0;
252 self.expanded_nodes = 0;
253 }
254
255 #[allow(clippy::indexing_slicing)] fn run(&mut self, input: &str) -> Result<Vec<Document<Span>>> {
257 let raw: std::result::Result<Vec<_>, _> = crate::parse_events(input).collect();
259 let events = raw.map_err(|e| LoadError::Parse {
260 pos: e.pos,
261 message: e.message,
262 })?;
263
264 let mut docs: Vec<Document<Span>> = Vec::new();
265 let mut pos = 0usize;
266
267 if let Some((Event::StreamStart, _)) = events.get(pos) {
269 pos += 1;
270 }
271
272 while pos < events.len() {
273 match &events[pos] {
274 (Event::StreamEnd, _) => break,
275 (Event::DocumentStart { version, tags, .. }, _) => {
276 let doc_version = *version;
277 let doc_tags = tags.clone();
278 pos += 1;
279 self.reset_for_document();
280
281 let mut doc_comments: Vec<String> = Vec::new();
282
283 while pos < events.len() {
285 match &events[pos] {
286 (Event::Comment { text }, _) => {
287 doc_comments.push(text.clone());
288 pos += 1;
289 }
290 _ => break,
291 }
292 }
293
294 let root = if matches!(
296 events.get(pos),
297 Some((Event::DocumentEnd { .. } | Event::StreamEnd, _)) | None
298 ) {
299 Node::Scalar {
301 value: String::new(),
302 style: ScalarStyle::Plain,
303 anchor: None,
304 tag: None,
305 loc: Span {
306 start: Pos::ORIGIN,
307 end: Pos::ORIGIN,
308 },
309 leading_comments: Vec::new(),
310 trailing_comment: None,
311 }
312 } else {
313 self.parse_node(&events, &mut pos)?
314 };
315
316 if matches!(events.get(pos), Some((Event::DocumentEnd { .. }, _))) {
318 pos += 1;
319 }
320
321 docs.push(Document {
322 root,
323 version: doc_version,
324 tags: doc_tags,
325 comments: doc_comments,
326 });
327 }
328 _ => {
329 pos += 1;
330 }
331 }
332 }
333
334 Ok(docs)
335 }
336
337 #[allow(clippy::too_many_lines)] fn parse_node(&mut self, events: &[(Event, Span)], pos: &mut usize) -> Result<Node<Span>> {
340 let Some((event, span)) = events.get(*pos) else {
341 return Ok(empty_scalar());
342 };
343 let span = *span;
344
345 match event {
346 Event::Scalar {
347 value,
348 style,
349 anchor,
350 tag,
351 } => {
352 let node = Node::Scalar {
353 value: value.clone(),
354 style: *style,
355 anchor: anchor.clone(),
356 tag: tag.clone(),
357 loc: span,
358 leading_comments: Vec::new(),
359 trailing_comment: None,
360 };
361 if let Some(name) = anchor {
362 self.register_anchor(name.clone(), node.clone())?;
363 }
364 *pos += 1;
365 Ok(node)
366 }
367
368 Event::MappingStart { anchor, tag } => {
369 let anchor = anchor.clone();
370 let tag = tag.clone();
371 *pos += 1;
372
373 self.depth += 1;
374 if self.depth > self.options.max_nesting_depth {
375 return Err(LoadError::NestingDepthLimitExceeded {
376 limit: self.options.max_nesting_depth,
377 });
378 }
379
380 let mut entries: Vec<(Node<Span>, Node<Span>)> = Vec::new();
381 while !matches!(events.get(*pos), Some((Event::MappingEnd, _)) | None) {
382 let leading = collect_leading_comments(events, pos);
384 let mut key = self.parse_node(events, pos)?;
385 attach_leading_comments(&mut key, leading);
386
387 let mut value = self.parse_node(events, pos)?;
388 if let Some(trail) = collect_trailing_comment(events, pos) {
390 attach_trailing_comment(&mut value, trail);
391 }
392
393 entries.push((key, value));
394 }
395 let end_span = if let Some((Event::MappingEnd, end)) = events.get(*pos) {
397 let s = *end;
398 *pos += 1;
399 s
400 } else {
401 span
402 };
403 self.depth -= 1;
404
405 let node = Node::Mapping {
406 entries,
407 anchor: anchor.clone(),
408 tag,
409 loc: Span {
410 start: span.start,
411 end: end_span.end,
412 },
413 leading_comments: Vec::new(),
414 trailing_comment: None,
415 };
416 if let Some(name) = anchor {
417 self.register_anchor(name, node.clone())?;
418 }
419 Ok(node)
420 }
421
422 Event::SequenceStart { anchor, tag } => {
423 let anchor = anchor.clone();
424 let tag = tag.clone();
425 *pos += 1;
426
427 self.depth += 1;
428 if self.depth > self.options.max_nesting_depth {
429 return Err(LoadError::NestingDepthLimitExceeded {
430 limit: self.options.max_nesting_depth,
431 });
432 }
433
434 let mut items: Vec<Node<Span>> = Vec::new();
435 while !matches!(events.get(*pos), Some((Event::SequenceEnd, _)) | None) {
436 let leading = collect_leading_comments(events, pos);
438 let mut item = self.parse_node(events, pos)?;
439 attach_leading_comments(&mut item, leading);
440 if let Some(trail) = collect_trailing_comment(events, pos) {
442 attach_trailing_comment(&mut item, trail);
443 }
444 items.push(item);
445 }
446 let end_span = if let Some((Event::SequenceEnd, end)) = events.get(*pos) {
448 let s = *end;
449 *pos += 1;
450 s
451 } else {
452 span
453 };
454 self.depth -= 1;
455
456 let node = Node::Sequence {
457 items,
458 anchor: anchor.clone(),
459 tag,
460 loc: Span {
461 start: span.start,
462 end: end_span.end,
463 },
464 leading_comments: Vec::new(),
465 trailing_comment: None,
466 };
467 if let Some(name) = anchor {
468 self.register_anchor(name, node.clone())?;
469 }
470 Ok(node)
471 }
472
473 Event::Alias { name } => {
474 let name = name.clone();
475 *pos += 1;
476 self.resolve_alias(&name, span)
477 }
478
479 Event::Comment { .. } => {
480 *pos += 1;
482 self.parse_node(events, pos)
483 }
484
485 Event::StreamStart
486 | Event::StreamEnd
487 | Event::DocumentStart { .. }
488 | Event::DocumentEnd { .. }
489 | Event::MappingEnd
490 | Event::SequenceEnd => {
491 *pos += 1;
493 self.parse_node(events, pos)
494 }
495 }
496 }
497
498 fn register_anchor(&mut self, name: String, node: Node<Span>) -> Result<()> {
499 if !self.anchor_map.contains_key(&name) {
500 self.anchor_count += 1;
501 if self.anchor_count > self.options.max_anchors {
502 return Err(LoadError::AnchorCountLimitExceeded {
503 limit: self.options.max_anchors,
504 });
505 }
506 }
507 if self.options.mode == LoadMode::Resolved {
511 self.expanded_nodes += 1;
512 if self.expanded_nodes > self.options.max_expanded_nodes {
513 return Err(LoadError::AliasExpansionLimitExceeded {
514 limit: self.options.max_expanded_nodes,
515 });
516 }
517 }
518 self.anchor_map.insert(name, node);
519 Ok(())
520 }
521
522 fn resolve_alias(&mut self, name: &str, loc: Span) -> Result<Node<Span>> {
523 match self.options.mode {
524 LoadMode::Lossless => Ok(Node::Alias {
525 name: name.to_owned(),
526 loc,
527 leading_comments: Vec::new(),
528 trailing_comment: None,
529 }),
530 LoadMode::Resolved => {
531 let anchored = self.anchor_map.get(name).cloned().ok_or_else(|| {
532 LoadError::UndefinedAlias {
533 name: name.to_owned(),
534 }
535 })?;
536 let mut in_progress: HashSet<String> = HashSet::new();
537 self.expand_node(anchored, &mut in_progress)
538 }
539 }
540 }
541
542 fn expand_node(
545 &mut self,
546 node: Node<Span>,
547 in_progress: &mut HashSet<String>,
548 ) -> Result<Node<Span>> {
549 self.expanded_nodes += 1;
550 if self.expanded_nodes > self.options.max_expanded_nodes {
551 return Err(LoadError::AliasExpansionLimitExceeded {
552 limit: self.options.max_expanded_nodes,
553 });
554 }
555
556 match node {
557 Node::Alias { ref name, loc, .. } => {
558 if in_progress.contains(name) {
559 return Err(LoadError::CircularAlias { name: name.clone() });
560 }
561 let target = self
562 .anchor_map
563 .get(name)
564 .cloned()
565 .ok_or_else(|| LoadError::UndefinedAlias { name: name.clone() })?;
566 in_progress.insert(name.clone());
567 let expanded = self.expand_node(target, in_progress)?;
568 in_progress.remove(name);
569 Ok(reloc(expanded, loc))
571 }
572 Node::Mapping {
573 entries,
574 anchor,
575 tag,
576 loc,
577 leading_comments,
578 trailing_comment,
579 } => {
580 let mut expanded_entries = Vec::with_capacity(entries.len());
581 for (k, v) in entries {
582 let ek = self.expand_node(k, in_progress)?;
583 let ev = self.expand_node(v, in_progress)?;
584 expanded_entries.push((ek, ev));
585 }
586 Ok(Node::Mapping {
587 entries: expanded_entries,
588 anchor,
589 tag,
590 loc,
591 leading_comments,
592 trailing_comment,
593 })
594 }
595 Node::Sequence {
596 items,
597 anchor,
598 tag,
599 loc,
600 leading_comments,
601 trailing_comment,
602 } => {
603 let mut expanded_items = Vec::with_capacity(items.len());
604 for item in items {
605 expanded_items.push(self.expand_node(item, in_progress)?);
606 }
607 Ok(Node::Sequence {
608 items: expanded_items,
609 anchor,
610 tag,
611 loc,
612 leading_comments,
613 trailing_comment,
614 })
615 }
616 scalar @ Node::Scalar { .. } => Ok(scalar),
618 }
619 }
620}
621
622const fn empty_scalar() -> Node<Span> {
627 Node::Scalar {
628 value: String::new(),
629 style: ScalarStyle::Plain,
630 anchor: None,
631 tag: None,
632 loc: Span {
633 start: Pos::ORIGIN,
634 end: Pos::ORIGIN,
635 },
636 leading_comments: Vec::new(),
637 trailing_comment: None,
638 }
639}
640
641fn reloc(node: Node<Span>, loc: Span) -> Node<Span> {
643 match node {
644 Node::Scalar {
645 value,
646 style,
647 anchor,
648 tag,
649 leading_comments,
650 trailing_comment,
651 ..
652 } => Node::Scalar {
653 value,
654 style,
655 anchor,
656 tag,
657 loc,
658 leading_comments,
659 trailing_comment,
660 },
661 Node::Mapping {
662 entries,
663 anchor,
664 tag,
665 leading_comments,
666 trailing_comment,
667 ..
668 } => Node::Mapping {
669 entries,
670 anchor,
671 tag,
672 loc,
673 leading_comments,
674 trailing_comment,
675 },
676 Node::Sequence {
677 items,
678 anchor,
679 tag,
680 leading_comments,
681 trailing_comment,
682 ..
683 } => Node::Sequence {
684 items,
685 anchor,
686 tag,
687 loc,
688 leading_comments,
689 trailing_comment,
690 },
691 Node::Alias {
692 name,
693 leading_comments,
694 trailing_comment,
695 ..
696 } => Node::Alias {
697 name,
698 loc,
699 leading_comments,
700 trailing_comment,
701 },
702 }
703}
704
705fn collect_leading_comments(events: &[(Event, Span)], pos: &mut usize) -> Vec<String> {
713 let mut leading = Vec::new();
714 while let Some((Event::Comment { text }, span)) = events.get(*pos) {
715 if span.end.line > span.start.line {
716 leading.push(format!("#{text}"));
717 *pos += 1;
718 } else {
719 break;
720 }
721 }
722 leading
723}
724
725fn collect_trailing_comment(events: &[(Event, Span)], pos: &mut usize) -> Option<String> {
728 if let Some((Event::Comment { text }, span)) = events.get(*pos) {
729 if span.start == span.end {
730 let result = format!("#{text}");
731 *pos += 1;
732 return Some(result);
733 }
734 }
735 None
736}
737
738fn attach_leading_comments(node: &mut Node<Span>, comments: Vec<String>) {
740 if comments.is_empty() {
741 return;
742 }
743 match node {
744 Node::Scalar {
745 leading_comments, ..
746 }
747 | Node::Mapping {
748 leading_comments, ..
749 }
750 | Node::Sequence {
751 leading_comments, ..
752 }
753 | Node::Alias {
754 leading_comments, ..
755 } => {
756 *leading_comments = comments;
757 }
758 }
759}
760
761fn attach_trailing_comment(node: &mut Node<Span>, comment: String) {
763 match node {
764 Node::Scalar {
765 trailing_comment, ..
766 }
767 | Node::Mapping {
768 trailing_comment, ..
769 }
770 | Node::Sequence {
771 trailing_comment, ..
772 }
773 | Node::Alias {
774 trailing_comment, ..
775 } => {
776 *trailing_comment = Some(comment);
777 }
778 }
779}
780
781#[cfg(test)]
786#[allow(
787 clippy::indexing_slicing,
788 clippy::expect_used,
789 clippy::unwrap_used,
790 clippy::too_many_lines,
791 clippy::doc_markdown
792)]
793mod tests {
794 use std::fmt::Write as _;
795
796 use super::*;
797 use crate::event::ScalarStyle;
798
799 const LIMIT: usize = 1_000_000;
801
802 fn load_one(input: &str) -> Node<Span> {
807 let docs = load(input).expect("load failed");
808 assert_eq!(docs.len(), 1, "expected 1 document, got {}", docs.len());
809 docs.into_iter().next().unwrap().root
810 }
811
812 fn load_resolved_one(input: &str) -> Node<Span> {
813 let docs = LoaderBuilder::new()
814 .resolved()
815 .build()
816 .load(input)
817 .expect("load failed");
818 assert_eq!(docs.len(), 1, "expected 1 document, got {}", docs.len());
819 docs.into_iter().next().unwrap().root
820 }
821
822 fn scalar_value(node: &Node<Span>) -> &str {
823 match node {
824 Node::Scalar { value, .. } => value.as_str(),
825 other @ (Node::Mapping { .. } | Node::Sequence { .. } | Node::Alias { .. }) => {
826 panic!("expected Scalar, got {other:?}")
827 }
828 }
829 }
830
831 #[test]
837 fn load_is_wired_into_lib_rs() {
838 let docs = crate::load("hello\n").expect("crate::load failed");
839 assert!(!docs.is_empty());
840 }
841
842 #[test]
844 fn load_returns_ok_for_valid_input() {
845 assert!(load("hello\n").is_ok());
846 }
847
848 #[test]
850 fn load_returns_vec_of_documents() {
851 let docs = load("hello\n").unwrap();
852 assert_eq!(docs.len(), 1);
853 }
854
855 #[test]
857 fn loader_builder_new_is_callable() {
858 let result = LoaderBuilder::new().build().load("hello\n");
859 assert!(result.is_ok());
860 }
861
862 #[test]
864 fn loader_builder_lossless_mode_is_callable() {
865 let result = LoaderBuilder::new().lossless().build().load("hello\n");
866 assert!(result.is_ok());
867 }
868
869 #[test]
871 fn loader_builder_resolved_mode_is_callable() {
872 let result = LoaderBuilder::new().resolved().build().load("hello\n");
873 assert!(result.is_ok());
874 }
875
876 #[test]
882 fn document_has_root_node() {
883 let docs = load("hello\n").unwrap();
884 let doc = docs.into_iter().next().unwrap();
885 assert!(matches!(doc.root, Node::Scalar { .. }));
886 }
887
888 #[test]
890 fn document_version_is_none_without_yaml_directive() {
891 let docs = load("hello\n").unwrap();
892 assert_eq!(docs[0].version, None);
893 }
894
895 #[test]
897 fn document_tags_is_empty_without_tag_directive() {
898 let docs = load("hello\n").unwrap();
899 assert!(docs[0].tags.is_empty());
900 }
901
902 #[test]
904 fn empty_input_returns_empty_vec() {
905 let docs = load("").unwrap();
906 assert!(docs.is_empty());
907 }
908
909 #[test]
911 fn multi_document_input_returns_multiple_documents() {
912 let docs = load("---\nfirst\n...\n---\nsecond\n...\n").unwrap();
913 assert_eq!(docs.len(), 2);
914 }
915
916 #[test]
922 fn plain_scalar_loads_as_scalar_node() {
923 let node = load_one("hello\n");
924 assert!(
925 matches!(&node, Node::Scalar { value, .. } if value == "hello"),
926 "got: {node:?}"
927 );
928 }
929
930 #[test]
932 fn scalar_node_style_is_plain_for_plain_scalar() {
933 let node = load_one("hello\n");
934 assert!(matches!(
935 node,
936 Node::Scalar {
937 style: ScalarStyle::Plain,
938 ..
939 }
940 ));
941 }
942
943 #[test]
945 fn single_quoted_scalar_loads_with_single_quoted_style() {
946 let node = load_one("'hello'\n");
947 assert!(matches!(
948 node,
949 Node::Scalar {
950 style: ScalarStyle::SingleQuoted,
951 ..
952 }
953 ));
954 }
955
956 #[test]
958 fn double_quoted_scalar_loads_with_double_quoted_style() {
959 let node = load_one("\"hello\"\n");
960 assert!(matches!(
961 node,
962 Node::Scalar {
963 style: ScalarStyle::DoubleQuoted,
964 ..
965 }
966 ));
967 }
968
969 #[test]
971 fn literal_block_scalar_loads_with_literal_style() {
972 let node = load_one("|\n hello\n");
973 assert!(
974 matches!(
975 node,
976 Node::Scalar {
977 style: ScalarStyle::Literal(_),
978 ..
979 }
980 ),
981 "got: {node:?}"
982 );
983 }
984
985 #[test]
987 fn folded_block_scalar_loads_with_folded_style() {
988 let node = load_one(">\n hello\n");
989 assert!(
990 matches!(
991 node,
992 Node::Scalar {
993 style: ScalarStyle::Folded(_),
994 ..
995 }
996 ),
997 "got: {node:?}"
998 );
999 }
1000
1001 #[test]
1003 fn scalar_node_tag_is_none_without_tag() {
1004 let node = load_one("hello\n");
1005 assert!(matches!(node, Node::Scalar { tag: None, .. }));
1006 }
1007
1008 #[test]
1010 fn tagged_scalar_has_tag_field() {
1011 let node = load_one("!!str hello\n");
1012 assert!(
1013 matches!(&node, Node::Scalar { tag: Some(t), .. } if t.contains("str")),
1014 "got: {node:?}"
1015 );
1016 }
1017
1018 #[test]
1024 fn block_mapping_loads_as_mapping_node() {
1025 let node = load_one("key: value\n");
1026 assert!(matches!(node, Node::Mapping { .. }), "got: {node:?}");
1027 }
1028
1029 #[test]
1031 fn mapping_has_correct_entry_count() {
1032 let node = load_one("{a: 1, b: 2}\n");
1036 assert!(
1037 matches!(&node, Node::Mapping { entries, .. } if entries.len() == 2),
1038 "got: {node:?}"
1039 );
1040 }
1041
1042 #[test]
1044 fn mapping_entry_key_and_value_are_scalars() {
1045 let node = load_one("key: value\n");
1046 let Node::Mapping { entries, .. } = node else {
1047 panic!("expected Mapping");
1048 };
1049 let (k, v) = &entries[0];
1050 assert!(matches!(k, Node::Scalar { value, .. } if value == "key"));
1051 assert!(matches!(v, Node::Scalar { value, .. } if value == "value"));
1052 }
1053
1054 #[test]
1056 fn mapping_entries_preserve_order() {
1057 let node = load_one("{a: 1, b: 2, c: 3}\n");
1060 let Node::Mapping { entries, .. } = node else {
1061 panic!("expected Mapping");
1062 };
1063 assert_eq!(entries.len(), 3);
1064 assert_eq!(scalar_value(&entries[0].0), "a");
1065 assert_eq!(scalar_value(&entries[1].0), "b");
1066 assert_eq!(scalar_value(&entries[2].0), "c");
1067 }
1068
1069 #[test]
1071 fn empty_mapping_has_zero_entries() {
1072 let node = load_one("{}\n");
1073 assert!(
1074 matches!(&node, Node::Mapping { entries, .. } if entries.is_empty()),
1075 "got: {node:?}"
1076 );
1077 }
1078
1079 #[test]
1081 fn nested_mapping_value_is_mapping_node() {
1082 let node = load_one("outer:\n inner: value\n");
1083 let Node::Mapping { entries, .. } = node else {
1084 panic!("expected Mapping");
1085 };
1086 assert!(matches!(&entries[0].1, Node::Mapping { .. }));
1087 }
1088
1089 #[test]
1091 fn mapping_anchor_is_none_without_anchor() {
1092 let node = load_one("key: value\n");
1093 assert!(matches!(node, Node::Mapping { anchor: None, .. }));
1094 }
1095
1096 #[test]
1098 fn flow_mapping_loads_as_mapping_node() {
1099 let node = load_one("{key: value}\n");
1100 assert!(matches!(node, Node::Mapping { .. }), "got: {node:?}");
1101 }
1102
1103 #[test]
1109 fn block_sequence_loads_as_sequence_node() {
1110 let node = load_one("- a\n- b\n");
1111 assert!(matches!(node, Node::Sequence { .. }), "got: {node:?}");
1112 }
1113
1114 #[test]
1116 fn sequence_has_correct_item_count() {
1117 let node = load_one("- a\n- b\n- c\n");
1118 assert!(
1119 matches!(&node, Node::Sequence { items, .. } if items.len() == 3),
1120 "got: {node:?}"
1121 );
1122 }
1123
1124 #[test]
1126 fn sequence_items_are_scalar_nodes() {
1127 let node = load_one("- a\n- b\n");
1128 let Node::Sequence { items, .. } = node else {
1129 panic!("expected Sequence");
1130 };
1131 assert!(matches!(&items[0], Node::Scalar { .. }));
1132 assert!(matches!(&items[1], Node::Scalar { .. }));
1133 }
1134
1135 #[test]
1137 fn sequence_items_preserve_order() {
1138 let node = load_one("- first\n- second\n- third\n");
1139 let Node::Sequence { items, .. } = node else {
1140 panic!("expected Sequence");
1141 };
1142 assert_eq!(scalar_value(&items[0]), "first");
1143 assert_eq!(scalar_value(&items[2]), "third");
1144 }
1145
1146 #[test]
1148 fn empty_sequence_has_zero_items() {
1149 let node = load_one("[]\n");
1150 assert!(
1151 matches!(&node, Node::Sequence { items, .. } if items.is_empty()),
1152 "got: {node:?}"
1153 );
1154 }
1155
1156 #[test]
1158 fn nested_sequence_item_is_sequence_node() {
1159 let node = load_one("- - a\n - b\n");
1160 let Node::Sequence { items, .. } = node else {
1161 panic!("expected Sequence");
1162 };
1163 assert!(
1164 matches!(&items[0], Node::Sequence { .. }),
1165 "got: {:?}",
1166 &items[0]
1167 );
1168 }
1169
1170 #[test]
1172 fn flow_sequence_loads_as_sequence_node() {
1173 let node = load_one("[a, b, c]\n");
1174 assert!(
1175 matches!(&node, Node::Sequence { items, .. } if items.len() == 3),
1176 "got: {node:?}"
1177 );
1178 }
1179
1180 #[test]
1186 fn anchored_scalar_preserves_anchor_field() {
1187 let node = load_one("&a hello\n");
1188 assert!(
1189 matches!(&node, Node::Scalar { anchor: Some(a), .. } if a == "a"),
1190 "got: {node:?}"
1191 );
1192 }
1193
1194 #[test]
1196 fn alias_reference_becomes_alias_node_in_lossless_mode() {
1197 let node = load_one("- &a hello\n- *a\n");
1198 let Node::Sequence { items, .. } = node else {
1199 panic!("expected Sequence");
1200 };
1201 assert_eq!(items.len(), 2);
1202 assert!(
1203 matches!(&items[1], Node::Alias { name, .. } if name == "a"),
1204 "got: {:?}",
1205 &items[1]
1206 );
1207 }
1208
1209 #[test]
1211 fn anchored_mapping_preserves_anchor_field() {
1212 let node = load_one("&m\nkey: value\n");
1213 assert!(
1214 matches!(&node, Node::Mapping { anchor: Some(a), .. } if a == "m"),
1215 "got: {node:?}"
1216 );
1217 }
1218
1219 #[test]
1221 fn anchored_sequence_preserves_anchor_field() {
1222 let node = load_one("&s\n- a\n- b\n");
1223 assert!(
1224 matches!(&node, Node::Sequence { anchor: Some(a), .. } if a == "s"),
1225 "got: {node:?}"
1226 );
1227 }
1228
1229 #[test]
1231 fn alias_node_name_matches_anchor() {
1232 let node = load_one("- &ref hello\n- *ref\n");
1233 let Node::Sequence { items, .. } = node else {
1234 panic!("expected Sequence");
1235 };
1236 assert!(
1237 matches!(&items[1], Node::Alias { name, .. } if name == "ref"),
1238 "got: {:?}",
1239 &items[1]
1240 );
1241 }
1242
1243 #[test]
1245 fn multiple_aliases_to_same_anchor_all_become_alias_nodes() {
1246 let node = load_one("- &a hello\n- *a\n- *a\n");
1247 let Node::Sequence { items, .. } = node else {
1248 panic!("expected Sequence");
1249 };
1250 assert!(matches!(&items[1], Node::Alias { name, .. } if name == "a"));
1251 assert!(matches!(&items[2], Node::Alias { name, .. } if name == "a"));
1252 }
1253
1254 #[test]
1256 fn alias_in_mapping_value_becomes_alias_node() {
1257 let node = load_one("- &a value\n- {ref: *a}\n");
1262 let Node::Sequence { items, .. } = node else {
1263 panic!("expected Sequence");
1264 };
1265 let Node::Mapping { entries, .. } = &items[1] else {
1266 panic!("expected Mapping in second item");
1267 };
1268 let ref_entry = entries.iter().find(|(k, _)| scalar_value(k) == "ref");
1269 assert!(ref_entry.is_some(), "key 'ref' not found");
1270 let (_, value) = ref_entry.unwrap();
1271 assert!(
1272 matches!(value, Node::Alias { name, .. } if name == "a"),
1273 "got: {value:?}"
1274 );
1275 }
1276
1277 #[test]
1279 fn lossless_mode_does_not_expand_aliases() {
1280 let node = load_one("- &a hello\n- *a\n");
1281 let Node::Sequence { items, .. } = node else {
1282 panic!("expected Sequence");
1283 };
1284 assert!(
1286 matches!(&items[1], Node::Alias { .. }),
1287 "expected Alias, got: {:?}",
1288 &items[1]
1289 );
1290 }
1291
1292 #[test]
1298 fn resolved_mode_expands_scalar_alias() {
1299 let node = load_resolved_one("- &a hello\n- *a\n");
1300 let Node::Sequence { items, .. } = node else {
1301 panic!("expected Sequence");
1302 };
1303 assert_eq!(items.len(), 2);
1304 assert!(matches!(&items[0], Node::Scalar { value, .. } if value == "hello"));
1305 assert!(matches!(&items[1], Node::Scalar { value, .. } if value == "hello"));
1306 }
1307
1308 #[test]
1310 fn resolved_mode_expanded_alias_matches_anchored_value() {
1311 let node = load_resolved_one("- &a world\n- {ref: *a}\n");
1315 let Node::Sequence { items, .. } = node else {
1316 panic!("expected Sequence");
1317 };
1318 let Node::Mapping { entries, .. } = &items[1] else {
1319 panic!("expected Mapping in second item");
1320 };
1321 let ref_entry = entries.iter().find(|(k, _)| scalar_value(k) == "ref");
1322 assert!(ref_entry.is_some(), "key 'ref' not found");
1323 let (_, value) = ref_entry.unwrap();
1324 assert!(
1325 matches!(value, Node::Scalar { value, .. } if value == "world"),
1326 "got: {value:?}"
1327 );
1328 }
1329
1330 #[test]
1332 fn resolved_mode_expands_mapping_alias() {
1333 let node = load_resolved_one("base: &b\n key: value\nmerge: *b\n");
1334 let Node::Mapping { entries, .. } = node else {
1335 panic!("expected Mapping");
1336 };
1337 let merge_entry = entries.iter().find(|(k, _)| scalar_value(k) == "merge");
1338 assert!(merge_entry.is_some(), "key 'merge' not found");
1339 let (_, value) = merge_entry.unwrap();
1340 assert!(matches!(value, Node::Mapping { .. }), "got: {value:?}");
1341 }
1342
1343 #[test]
1345 fn resolved_mode_expands_sequence_alias() {
1346 let node = load_resolved_one("- &b\n - a\n - b\n- {ref: *b}\n");
1349 let Node::Sequence { items, .. } = node else {
1350 panic!("expected Sequence");
1351 };
1352 let Node::Mapping { entries, .. } = &items[1] else {
1353 panic!("expected Mapping in second item");
1354 };
1355 let ref_entry = entries.iter().find(|(k, _)| scalar_value(k) == "ref");
1356 assert!(ref_entry.is_some(), "key 'ref' not found");
1357 let (_, value) = ref_entry.unwrap();
1358 assert!(
1359 matches!(value, Node::Sequence { items, .. } if items.len() == 2),
1360 "got: {value:?}"
1361 );
1362 }
1363
1364 #[test]
1366 fn resolved_mode_multiple_expansions_are_independent_copies() {
1367 let node = load_resolved_one("- &a hello\n- *a\n- *a\n");
1368 let Node::Sequence { items, .. } = node else {
1369 panic!("expected Sequence");
1370 };
1371 assert!(matches!(&items[1], Node::Scalar { value, .. } if value == "hello"));
1372 assert!(matches!(&items[2], Node::Scalar { value, .. } if value == "hello"));
1373 }
1374
1375 #[test]
1377 fn resolved_mode_anchor_field_preserved_on_defining_node() {
1378 let node = load_resolved_one("- &a hello\n- *a\n");
1379 let Node::Sequence { items, .. } = node else {
1380 panic!("expected Sequence");
1381 };
1382 assert!(
1383 matches!(&items[0], Node::Scalar { anchor: Some(a), .. } if a == "a"),
1384 "got: {:?}",
1385 &items[0]
1386 );
1387 }
1388
1389 #[test]
1394 fn resolved_mode_below_limit_succeeds() {
1395 let custom_limit = 100usize;
1399 let refs = (0..98).map(|_| "- *a\n").collect::<String>();
1401 let yaml = format!("- &a x\n{refs}");
1402 let result = LoaderBuilder::new()
1403 .resolved()
1404 .max_expanded_nodes(custom_limit)
1405 .build()
1406 .load(&yaml);
1407 assert!(result.is_ok(), "expected Ok, got: {result:?}");
1408 }
1409
1410 #[test]
1412 fn resolved_mode_at_limit_is_rejected() {
1413 let custom_limit = 10usize;
1414 let refs = (0..10).map(|_| "- *a\n").collect::<String>();
1416 let yaml = format!("- &a x\n{refs}");
1417 let result = LoaderBuilder::new()
1418 .resolved()
1419 .max_expanded_nodes(custom_limit)
1420 .build()
1421 .load(&yaml);
1422 assert!(result.is_err(), "expected Err at limit, got Ok: {result:?}");
1423 assert!(matches!(
1424 result.unwrap_err(),
1425 LoadError::AliasExpansionLimitExceeded { .. }
1426 ));
1427 }
1428
1429 #[test]
1435 fn alias_bomb_three_levels_is_rejected_in_resolved_mode() {
1436 let yaml = "- &a small\n- &b [*a, *a, *a]\n- &c [*b, *b, *b]\n- *c\n";
1440 let result = LoaderBuilder::new()
1441 .resolved()
1442 .max_expanded_nodes(20)
1443 .build()
1444 .load(yaml);
1445 assert!(
1446 result.is_err(),
1447 "expected Err for 3-level bomb with limit=20"
1448 );
1449 }
1450
1451 #[test]
1453 fn alias_bomb_nine_levels_nine_aliases_is_rejected() {
1454 let yaml = concat!(
1455 "a: &a [\"lol\"]\n",
1456 "b: &b [*a, *a, *a, *a, *a, *a, *a, *a, *a]\n",
1457 "c: &c [*b, *b, *b, *b, *b, *b, *b, *b, *b]\n",
1458 "d: &d [*c, *c, *c, *c, *c, *c, *c, *c, *c]\n",
1459 "e: &e [*d, *d, *d, *d, *d, *d, *d, *d, *d]\n",
1460 "f: &f [*e, *e, *e, *e, *e, *e, *e, *e, *e]\n",
1461 "g: &g [*f, *f, *f, *f, *f, *f, *f, *f, *f]\n",
1462 "h: &h [*g, *g, *g, *g, *g, *g, *g, *g, *g]\n",
1463 "i: &i [*h, *h, *h, *h, *h, *h, *h, *h, *h]\n",
1464 "j: *i\n",
1465 );
1466 let result = LoaderBuilder::new().resolved().build().load(yaml);
1467 assert!(result.is_err(), "expected Err for 9-level bomb");
1468 assert!(matches!(
1469 result.unwrap_err(),
1470 LoadError::AliasExpansionLimitExceeded { .. }
1471 ));
1472 }
1473
1474 #[test]
1476 fn alias_bomb_is_accepted_in_lossless_mode() {
1477 let yaml = concat!(
1478 "a: &a [\"lol\"]\n",
1479 "b: &b [*a, *a, *a, *a, *a, *a, *a, *a, *a]\n",
1480 "c: &c [*b, *b, *b, *b, *b, *b, *b, *b, *b]\n",
1481 "d: &d [*c, *c, *c, *c, *c, *c, *c, *c, *c]\n",
1482 "e: &e [*d, *d, *d, *d, *d, *d, *d, *d, *d]\n",
1483 "f: &f [*e, *e, *e, *e, *e, *e, *e, *e, *e]\n",
1484 "g: &g [*f, *f, *f, *f, *f, *f, *f, *f, *f]\n",
1485 "h: &h [*g, *g, *g, *g, *g, *g, *g, *g, *g]\n",
1486 "i: &i [*h, *h, *h, *h, *h, *h, *h, *h, *h]\n",
1487 "j: *i\n",
1488 );
1489 let result = load(yaml);
1491 assert!(result.is_ok(), "expected Ok in lossless mode: {result:?}");
1492 }
1493
1494 #[test]
1496 fn alias_bomb_error_message_is_meaningful() {
1497 let yaml = concat!(
1498 "a: &a [\"lol\"]\n",
1499 "b: &b [*a, *a, *a, *a, *a, *a, *a, *a, *a]\n",
1500 "c: &c [*b, *b, *b, *b, *b, *b, *b, *b, *b]\n",
1501 "d: &d [*c, *c, *c, *c, *c, *c, *c, *c, *c]\n",
1502 "e: &e [*d, *d, *d, *d, *d, *d, *d, *d, *d]\n",
1503 "f: &f [*e, *e, *e, *e, *e, *e, *e, *e, *e]\n",
1504 "g: &g [*f, *f, *f, *f, *f, *f, *f, *f, *f]\n",
1505 "h: &h [*g, *g, *g, *g, *g, *g, *g, *g, *g]\n",
1506 "i: &i [*h, *h, *h, *h, *h, *h, *h, *h, *h]\n",
1507 "j: *i\n",
1508 );
1509 let result = LoaderBuilder::new().resolved().build().load(yaml);
1510 let err = result.expect_err("expected Err");
1511 let msg = err.to_string();
1512 assert!(!msg.is_empty(), "error message is empty");
1513 }
1514
1515 #[test]
1522 fn self_referencing_anchor_via_merge_key_is_rejected() {
1523 let custom_limit = 5usize;
1526 let refs = (0..5).map(|_| "- *a\n").collect::<String>();
1527 let yaml = format!("- &a x\n{refs}");
1528 let result = LoaderBuilder::new()
1529 .resolved()
1530 .max_expanded_nodes(custom_limit)
1531 .build()
1532 .load(&yaml);
1533 assert!(result.is_err(), "expected Err");
1534 }
1535
1536 #[test]
1538 fn deeply_nested_alias_chain_is_rejected_in_resolved_mode() {
1539 let custom_limit = LIMIT;
1540 let tiny_limit = 3usize;
1545 let yaml = "a: &a x\nb: &b [*a, *a, *a, *a]\n";
1546 let result = LoaderBuilder::new()
1547 .resolved()
1548 .max_expanded_nodes(tiny_limit)
1549 .build()
1550 .load(yaml);
1551 assert!(result.is_err(), "expected Err; tiny_limit={tiny_limit}");
1552 let _ = custom_limit; }
1554
1555 #[test]
1557 fn deeply_nested_alias_chain_succeeds_in_lossless_mode() {
1558 let yaml = "a: &a x\nb: &b [*a, *a, *a, *a]\n";
1559 let result = load(yaml);
1560 assert!(result.is_ok(), "expected Ok in lossless mode: {result:?}");
1561 }
1562
1563 #[test]
1565 fn unknown_alias_reference_returns_error() {
1566 let result = LoaderBuilder::new()
1569 .resolved()
1570 .build()
1571 .load("- *nonexistent\n");
1572 assert!(result.is_err(), "expected Err for unknown alias");
1573 }
1574
1575 #[test]
1577 fn unknown_alias_error_contains_alias_name() {
1578 let result = LoaderBuilder::new()
1579 .resolved()
1580 .build()
1581 .load("- *nonexistent\n");
1582 let err = result.expect_err("expected Err");
1583 let msg = err.to_string();
1584 assert!(
1585 msg.contains("nonexistent"),
1586 "error message should contain alias name; got: {msg:?}"
1587 );
1588 }
1589
1590 #[test]
1596 fn two_document_stream_returns_two_documents() {
1597 let docs = load("---\nfirst\n...\n---\nsecond\n...\n").unwrap();
1598 assert_eq!(docs.len(), 2);
1599 }
1600
1601 #[test]
1603 fn first_document_root_is_first_scalar() {
1604 let docs = load("---\nfirst\n...\n---\nsecond\n...\n").unwrap();
1605 assert!(
1606 matches!(&docs[0].root, Node::Scalar { value, .. } if value == "first"),
1607 "got: {:?}",
1608 &docs[0].root
1609 );
1610 }
1611
1612 #[test]
1614 fn second_document_root_is_second_scalar() {
1615 let docs = load("---\nfirst\n...\n---\nsecond\n...\n").unwrap();
1616 assert!(
1617 matches!(&docs[1].root, Node::Scalar { value, .. } if value == "second"),
1618 "got: {:?}",
1619 &docs[1].root
1620 );
1621 }
1622
1623 #[test]
1629 fn anchor_in_first_document_does_not_resolve_in_second() {
1630 let docs = load("---\n- &a hello\n...\n---\n- *a\n...\n").unwrap();
1632 assert_eq!(docs.len(), 2);
1633 let Node::Sequence { items, .. } = &docs[1].root else {
1634 panic!("expected Sequence in doc 2");
1635 };
1636 assert!(
1637 matches!(&items[0], Node::Alias { name, .. } if name == "a"),
1638 "got: {:?}",
1639 &items[0]
1640 );
1641
1642 let result = LoaderBuilder::new()
1644 .resolved()
1645 .build()
1646 .load("---\n- &a hello\n...\n---\n- *a\n...\n");
1647 assert!(result.is_err(), "expected Err in resolved mode");
1648 }
1649
1650 #[test]
1652 fn documents_have_independent_anchor_namespaces() {
1653 let docs = load("---\n&a hello\n...\n---\n&a world\n...\n").unwrap();
1654 assert_eq!(docs.len(), 2);
1655 assert!(matches!(&docs[0].root, Node::Scalar { anchor: Some(a), .. } if a == "a"));
1656 assert!(matches!(&docs[1].root, Node::Scalar { anchor: Some(a), .. } if a == "a"));
1657 }
1658
1659 #[test]
1670 fn comment_before_scalar_is_accessible_in_document() {
1671 let result = load("|\n hello\n# a comment\n");
1678 assert!(result.is_ok(), "expected Ok: {result:?}");
1679 let docs = result.unwrap();
1681 assert_eq!(docs.len(), 1);
1682 }
1683
1684 #[test]
1686 fn comment_after_block_scalar_is_accessible() {
1687 let result = load("|\n hello\n# trailing comment\n");
1688 assert!(result.is_ok(), "expected Ok: {result:?}");
1689 }
1690
1691 #[test]
1693 fn comments_do_not_interfere_with_node_values() {
1694 let node = load_one("hello\n");
1697 assert!(
1698 matches!(&node, Node::Scalar { value, .. } if value == "hello"),
1699 "got: {node:?}"
1700 );
1701 }
1702
1703 #[test]
1705 fn multiple_comments_do_not_cause_errors() {
1706 let result = load("|\n a\n# first\n---\n|\n b\n# second\n");
1710 assert!(result.is_ok(), "expected Ok: {result:?}");
1711 let docs = result.unwrap();
1712 assert_eq!(docs.len(), 2);
1713 }
1714
1715 #[test]
1721 fn error_type_implements_display() {
1722 let err = LoadError::UndefinedAlias {
1723 name: "foo".to_owned(),
1724 };
1725 let s = err.to_string();
1726 assert!(!s.is_empty());
1727 assert!(s.contains("foo"));
1728 }
1729
1730 #[test]
1732 fn error_has_position_field() {
1733 let err = LoadError::Parse {
1734 pos: Pos::ORIGIN,
1735 message: "oops".to_owned(),
1736 };
1737 assert!(err.to_string().contains("oops"));
1738 if let LoadError::Parse { pos, .. } = err {
1740 assert_eq!(pos, Pos::ORIGIN);
1741 }
1742 }
1743
1744 #[test]
1746 fn load_returns_ok_for_complex_valid_input() {
1747 let result = load("key: value\nlist:\n - a\n - b\nnested:\n inner: 42\n");
1748 assert!(result.is_ok(), "got: {result:?}");
1749 let docs = result.unwrap();
1750 assert_eq!(docs.len(), 1);
1751 }
1752
1753 #[test]
1755 fn load_handles_explicit_null() {
1756 let result = load("key:\n");
1757 assert!(result.is_ok(), "got: {result:?}");
1758 }
1759
1760 #[test]
1762 fn load_handles_all_scalar_styles() {
1763 let result = load(
1764 "plain: hello\nsingle: 'world'\ndouble: \"foo\"\nliteral: |\n bar\nfolded: >\n baz\n",
1765 );
1766 assert!(result.is_ok(), "got: {result:?}");
1767 }
1768
1769 #[test]
1771 fn load_handles_unicode_scalar_value() {
1772 let docs = load("value: こんにちは\n").unwrap();
1773 let Node::Mapping { entries, .. } = &docs[0].root else {
1774 panic!("expected Mapping");
1775 };
1776 let val_entry = entries
1777 .iter()
1778 .find(|(k, _)| scalar_value(k) == "value")
1779 .expect("key 'value' not found");
1780 assert!(
1781 matches!(&val_entry.1, Node::Scalar { value, .. } if value == "こんにちは"),
1782 "got: {:?}",
1783 &val_entry.1
1784 );
1785 }
1786
1787 #[test]
1793 fn load_is_accessible_from_crate_root() {
1794 let result = crate::load("hello\n");
1795 assert!(result.is_ok());
1796 }
1797
1798 #[test]
1800 fn load_full_document_structure_is_correct() {
1801 let docs = load("key: value\n").unwrap();
1802 assert_eq!(docs.len(), 1);
1803 let Node::Mapping { entries, .. } = &docs[0].root else {
1804 panic!("expected Mapping");
1805 };
1806 assert_eq!(entries.len(), 1);
1807 assert!(matches!(&entries[0].0, Node::Scalar { value, .. } if value == "key"));
1808 assert!(matches!(&entries[0].1, Node::Scalar { value, .. } if value == "value"));
1809 }
1810
1811 #[test]
1813 fn load_nested_document_tree_is_correct() {
1814 let docs = load("outer:\n - a\n - b\n").unwrap();
1815 let Node::Mapping { entries, .. } = &docs[0].root else {
1816 panic!("expected Mapping");
1817 };
1818 assert!(matches!(&entries[0].1, Node::Sequence { items, .. } if items.len() == 2));
1819 }
1820
1821 #[test]
1823 fn load_anchored_and_aliased_document_is_correct_in_lossless() {
1824 let docs = load("- &a hello\n- *a\n").unwrap();
1825 let Node::Sequence { items, .. } = &docs[0].root else {
1826 panic!("expected Sequence");
1827 };
1828 assert!(
1829 matches!(&items[0], Node::Scalar { value, anchor: Some(a), .. }
1830 if value == "hello" && a == "a")
1831 );
1832 assert!(matches!(&items[1], Node::Alias { name, .. } if name == "a"));
1833 }
1834
1835 #[test]
1837 fn load_anchored_and_aliased_document_is_correct_in_resolved() {
1838 let docs = LoaderBuilder::new()
1839 .resolved()
1840 .build()
1841 .load("- &a hello\n- *a\n")
1842 .unwrap();
1843 let Node::Sequence { items, .. } = &docs[0].root else {
1844 panic!("expected Sequence");
1845 };
1846 assert!(matches!(&items[0], Node::Scalar { value, .. } if value == "hello"));
1847 assert!(matches!(&items[1], Node::Scalar { value, .. } if value == "hello"));
1848 }
1849
1850 #[test]
1856 fn nesting_depth_limit_rejects_deep_structure() {
1857 let depth = 20usize;
1861 let yaml = "[".repeat(depth) + "x" + &"]".repeat(depth) + "\n";
1862 let result = LoaderBuilder::new()
1863 .max_nesting_depth(10)
1864 .build()
1865 .load(&yaml);
1866 assert!(result.is_err(), "expected Err for {depth}-deep nesting");
1867 assert!(matches!(
1868 result.unwrap_err(),
1869 LoadError::NestingDepthLimitExceeded { .. }
1870 ));
1871 }
1872
1873 #[test]
1875 fn anchor_count_limit_rejects_excess_anchors() {
1876 let mut yaml = String::new();
1879 for i in 0..=10 {
1880 let _ = writeln!(yaml, "- &a{i} x{i}");
1881 }
1882 let result = LoaderBuilder::new().max_anchors(10).build().load(&yaml);
1883 assert!(result.is_err(), "expected Err for 11 anchors with limit=10");
1884 assert!(matches!(
1885 result.unwrap_err(),
1886 LoadError::AnchorCountLimitExceeded { .. }
1887 ));
1888 }
1889
1890 #[test]
1892 fn custom_expansion_limit_is_respected() {
1893 let refs = (0..10).map(|_| "- *a\n").collect::<String>();
1894 let yaml = format!("- &a x\n{refs}");
1895 let result = LoaderBuilder::new()
1896 .resolved()
1897 .max_expanded_nodes(10)
1898 .build()
1899 .load(&yaml);
1900 assert!(result.is_err(), "expected Err with limit=10");
1901 assert!(matches!(
1902 result.unwrap_err(),
1903 LoadError::AliasExpansionLimitExceeded { .. }
1904 ));
1905 }
1906}
1907
1908#[cfg(test)]
1913#[allow(
1914 clippy::indexing_slicing,
1915 clippy::expect_used,
1916 clippy::unwrap_used,
1917 clippy::doc_markdown
1918)]
1919mod comment_tests {
1920 use super::*;
1921
1922 #[test]
1924 fn trailing_comment_on_mapping_value_attached_to_value_node() {
1925 let docs = load("a: 1 # note\nb: 2\n").unwrap();
1926 let root = &docs[0].root;
1927 let Node::Mapping { entries, .. } = root else {
1928 panic!("expected Mapping, got {root:?}");
1929 };
1930 assert_eq!(entries.len(), 2);
1931 let (_, val_a) = &entries[0];
1933 assert_eq!(
1934 val_a.trailing_comment(),
1935 Some("# note"),
1936 "value 'a' trailing comment: {val_a:?}"
1937 );
1938 let (_, val_b) = &entries[1];
1940 assert_eq!(
1941 val_b.trailing_comment(),
1942 None,
1943 "value 'b' should have no trailing comment: {val_b:?}"
1944 );
1945 }
1946
1947 #[test]
1949 fn leading_comment_before_non_first_mapping_key_attached_to_key_node() {
1950 let docs = load("a: 1\n# before b\nb: 2\n").unwrap();
1951 let root = &docs[0].root;
1952 let Node::Mapping { entries, .. } = root else {
1953 panic!("expected Mapping, got {root:?}");
1954 };
1955 assert_eq!(entries.len(), 2);
1956 let (key_a, _) = &entries[0];
1958 assert!(
1959 key_a.leading_comments().is_empty(),
1960 "key 'a' should have no leading comments: {key_a:?}"
1961 );
1962 let (key_b, _) = &entries[1];
1964 assert_eq!(
1965 key_b.leading_comments(),
1966 &["# before b"],
1967 "key 'b' leading comments: {key_b:?}"
1968 );
1969 }
1970
1971 #[test]
1973 fn scalar_with_no_comments_has_empty_fields() {
1974 let docs = load("key: value\n").unwrap();
1975 let root = &docs[0].root;
1976 let Node::Mapping { entries, .. } = root else {
1977 panic!("expected Mapping");
1978 };
1979 for (k, v) in entries {
1980 assert!(
1981 k.leading_comments().is_empty(),
1982 "key has unexpected leading comments"
1983 );
1984 assert!(
1985 k.trailing_comment().is_none(),
1986 "key has unexpected trailing comment"
1987 );
1988 assert!(
1989 v.leading_comments().is_empty(),
1990 "value has unexpected leading comments"
1991 );
1992 assert!(
1993 v.trailing_comment().is_none(),
1994 "value has unexpected trailing comment"
1995 );
1996 }
1997 }
1998
1999 #[test]
2001 fn multiple_leading_comments_before_non_first_key_all_attached() {
2002 let docs = load("a: 1\n# first\n# second\nb: 2\n").unwrap();
2003 let root = &docs[0].root;
2004 let Node::Mapping { entries, .. } = root else {
2005 panic!("expected Mapping");
2006 };
2007 let (key_b, _) = &entries[1];
2008 assert_eq!(
2009 key_b.leading_comments(),
2010 &["# first", "# second"],
2011 "key 'b' leading comments: {key_b:?}"
2012 );
2013 }
2014
2015 #[test]
2017 fn trailing_comment_on_sequence_item_attached_to_item_node() {
2018 let docs = load("- a # first item\n- b\n").unwrap();
2019 let root = &docs[0].root;
2020 let Node::Sequence { items, .. } = root else {
2021 panic!("expected Sequence, got {root:?}");
2022 };
2023 assert_eq!(items.len(), 2);
2024 assert_eq!(
2025 items[0].trailing_comment(),
2026 Some("# first item"),
2027 "item 0 trailing comment: {:?}",
2028 items[0]
2029 );
2030 assert_eq!(
2031 items[1].trailing_comment(),
2032 None,
2033 "item 1 should have no trailing comment: {:?}",
2034 items[1]
2035 );
2036 }
2037
2038 #[test]
2040 fn leading_comment_before_non_first_sequence_item_attached_to_item_node() {
2041 let docs = load("- one\n# between\n- two\n").unwrap();
2042 let root = &docs[0].root;
2043 let Node::Sequence { items, .. } = root else {
2044 panic!("expected Sequence, got {root:?}");
2045 };
2046 assert_eq!(items.len(), 2);
2047 assert!(
2048 items[0].leading_comments().is_empty(),
2049 "item 0 should have no leading comments: {:?}",
2050 items[0]
2051 );
2052 assert_eq!(
2053 items[1].leading_comments(),
2054 &["# between"],
2055 "item 1 leading comments: {:?}",
2056 items[1]
2057 );
2058 }
2059
2060 #[test]
2062 fn comment_text_stored_with_hash_prefix() {
2063 let docs = load("a: 1 # my note\nb: 2\n").unwrap();
2064 let root = &docs[0].root;
2065 let Node::Mapping { entries, .. } = root else {
2066 panic!("expected Mapping");
2067 };
2068 let (_, val_a) = &entries[0];
2069 let trail = val_a.trailing_comment().expect("expected trailing comment");
2070 assert!(
2071 trail.starts_with('#'),
2072 "trailing comment should start with '#': {trail:?}"
2073 );
2074 assert_eq!(trail, "# my note");
2075 }
2076
2077 #[test]
2080 fn document_prefix_leading_comment_not_in_doc_comments_and_not_on_nodes() {
2081 let docs = load("# preamble\nkey: value\n").unwrap();
2082 assert!(
2084 docs[0].comments.is_empty(),
2085 "doc.comments should be empty: {:?}",
2086 docs[0].comments
2087 );
2088 assert!(
2090 docs[0].root.leading_comments().is_empty(),
2091 "root leading_comments should be empty: {:?}",
2092 docs[0].root.leading_comments()
2093 );
2094 }
2095
2096 #[test]
2098 fn comment_between_documents_not_silently_lost() {
2099 let docs = load("first: 1\n---\n# between docs\nsecond: 2\n").unwrap();
2100 assert_eq!(docs.len(), 2, "expected 2 documents");
2101 let in_doc_comments = docs[1].comments.iter().any(|c| c.contains("between docs"));
2102 let in_root_leading = docs[1]
2103 .root
2104 .leading_comments()
2105 .iter()
2106 .any(|c| c.contains("between docs"));
2107 assert!(
2108 in_doc_comments || in_root_leading,
2109 "between-document comment should be captured in doc.comments or root \
2110 leading_comments, but was silently lost. doc[1].comments={:?}, \
2111 root.leading_comments()={:?}",
2112 docs[1].comments,
2113 docs[1].root.leading_comments()
2114 );
2115 }
2116}