1use super::{Lang, Mapping, Scalar, Sequence, SyntaxNode, TaggedNode};
2use crate::as_yaml::{AsYaml, YamlKind};
3use crate::error::YamlResult;
4use crate::lex::SyntaxKind;
5use crate::yaml::YamlFile;
6use rowan::ast::AstNode;
7use rowan::GreenNodeBuilder;
8use std::path::Path;
9
10ast_node!(Document, DOCUMENT, "A single YAML document");
11
12impl Document {
13 pub fn new() -> Document {
15 let mut builder = GreenNodeBuilder::new();
16 builder.start_node(SyntaxKind::DOCUMENT.into());
17 builder.token(SyntaxKind::DOC_START.into(), "---");
19 builder.token(SyntaxKind::WHITESPACE.into(), "\n");
20 builder.finish_node();
21 Document(SyntaxNode::new_root_mut(builder.finish()))
22 }
23
24 pub fn new_mapping() -> Document {
26 let mut builder = GreenNodeBuilder::new();
27 builder.start_node(SyntaxKind::DOCUMENT.into());
28 builder.start_node(SyntaxKind::MAPPING.into());
30 builder.finish_node(); builder.finish_node(); Document(SyntaxNode::new_root_mut(builder.finish()))
33 }
34
35 pub fn from_file<P: AsRef<Path>>(path: P) -> YamlResult<Document> {
43 use std::str::FromStr;
44 let content = std::fs::read_to_string(path)?;
45 Self::from_str(&content)
46 }
47
48 pub fn from_str_relaxed(s: &str) -> (Document, Vec<String>) {
67 let parsed = YamlFile::parse(s);
68 let errors = parsed.errors();
69 let first = parsed.tree().documents().next().unwrap_or_default();
70 (first, errors)
71 }
72
73 pub fn from_str_ignoring_comments(s: &str) -> YamlResult<Document> {
93 let parsed = YamlFile::parse(s);
94
95 if !parsed.positioned_errors().is_empty() {
96 let first_error = &parsed.positioned_errors()[0];
97 let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
98 return Err(crate::error::YamlError::Parse {
99 message: first_error.message.clone(),
100 line: Some(lc.line),
101 column: Some(lc.column),
102 });
103 }
104
105 let mut docs = parsed.tree().documents();
106 let first = docs.next().unwrap_or_default();
107
108 if docs.next().is_some() {
109 return Err(crate::error::YamlError::InvalidOperation {
110 operation: "Document::from_str_ignoring_comments".to_string(),
111 reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
112 });
113 }
114
115 Ok(first)
116 }
117
118 pub fn from_file_relaxed<P: AsRef<Path>>(
123 path: P,
124 ) -> Result<(Document, Vec<String>), std::io::Error> {
125 let content = std::fs::read_to_string(path)?;
126 Ok(Self::from_str_relaxed(&content))
127 }
128
129 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> YamlResult<()> {
134 let path = path.as_ref();
135 if let Some(parent) = path.parent() {
136 std::fs::create_dir_all(parent)?;
137 }
138 let mut content = self.to_string();
139 if !content.ends_with('\n') {
141 content.push('\n');
142 }
143 std::fs::write(path, content)?;
144 Ok(())
145 }
146
147 pub(crate) fn root_node(&self) -> Option<SyntaxNode> {
149 self.0.children().find(|child| {
150 matches!(
151 child.kind(),
152 SyntaxKind::MAPPING
153 | SyntaxKind::SEQUENCE
154 | SyntaxKind::SCALAR
155 | SyntaxKind::ALIAS
156 | SyntaxKind::TAGGED_NODE
157 )
158 })
159 }
160
161 fn root_value_node(&self) -> Option<SyntaxNode> {
167 let node = self.root_node()?;
168 if node.kind() != SyntaxKind::TAGGED_NODE {
169 return Some(node);
170 }
171 node.children().find(|child| {
172 matches!(
173 child.kind(),
174 SyntaxKind::MAPPING | SyntaxKind::SEQUENCE | SyntaxKind::SCALAR | SyntaxKind::ALIAS
175 )
176 })
177 }
178
179 pub fn as_mapping(&self) -> Option<Mapping> {
185 self.root_value_node().and_then(Mapping::cast)
186 }
187
188 pub fn as_sequence(&self) -> Option<Sequence> {
190 self.root_value_node().and_then(Sequence::cast)
191 }
192
193 pub fn as_scalar(&self) -> Option<Scalar> {
195 self.root_value_node().and_then(Scalar::cast)
196 }
197
198 pub fn as_tagged(&self) -> Option<crate::yaml::TaggedNode> {
201 self.root_node()
202 .filter(|node| node.kind() == SyntaxKind::TAGGED_NODE)
203 .and_then(crate::yaml::TaggedNode::cast)
204 }
205
206 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
208 self.as_mapping().is_some_and(|m| m.contains_key(key))
209 }
210
211 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
215 self.as_mapping().and_then(|m| m.get(key))
216 }
217
218 pub(crate) fn get_node(&self, key: impl crate::AsYaml) -> Option<SyntaxNode> {
223 self.as_mapping().and_then(|m| m.get_node(key))
224 }
225
226 pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) -> bool {
233 if let Some(mapping) = self.as_mapping() {
234 mapping.set(key, value);
235 true
236 } else if self.root_node().is_none() {
237 let mapping = Mapping::new();
238 mapping.set(key, value);
239 let child_count = self.0.children_with_tokens().count();
240 self.0
241 .splice_children(child_count..child_count, vec![mapping.0.into()]);
242 true
243 } else {
244 false
245 }
246 }
247
248 pub fn set_with_field_order<I, K>(
257 &self,
258 key: impl crate::AsYaml,
259 value: impl crate::AsYaml,
260 field_order: I,
261 ) -> bool
262 where
263 I: IntoIterator<Item = K>,
264 K: crate::AsYaml,
265 {
266 let field_order: Vec<K> = field_order.into_iter().collect();
268 if let Some(mapping) = self.as_mapping() {
269 mapping.set_with_field_order(key, value, field_order);
270 true
271 } else if self.root_node().is_none() {
272 let mapping = Mapping::new();
273 mapping.set_with_field_order(key, value, field_order);
274 let child_count = self.0.children_with_tokens().count();
275 self.0
276 .splice_children(child_count..child_count, vec![mapping.0.into()]);
277 true
278 } else {
279 false
280 }
281 }
282
283 pub fn remove(&self, key: impl crate::AsYaml) -> Option<super::MappingEntry> {
290 self.as_mapping()?.remove(key)
291 }
292
293 pub(crate) fn key_nodes(&self) -> impl Iterator<Item = SyntaxNode> + '_ {
295 self.as_mapping()
296 .into_iter()
297 .flat_map(|m| m.key_nodes().collect::<Vec<_>>())
298 }
299
300 pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
306 self.key_nodes().filter_map(|key_node| {
307 key_node
308 .children()
309 .next()
310 .and_then(crate::as_yaml::YamlNode::from_syntax)
311 })
312 }
313
314 pub fn is_empty(&self) -> bool {
316 self.as_mapping().map_or(true, |m| m.is_empty())
317 }
318
319 pub fn from_mapping(mapping: Mapping) -> Self {
321 let mut builder = GreenNodeBuilder::new();
323 builder.start_node(SyntaxKind::DOCUMENT.into());
324 builder.token(SyntaxKind::DOC_START.into(), "---");
326 builder.token(SyntaxKind::WHITESPACE.into(), "\n");
327
328 builder.start_node(SyntaxKind::MAPPING.into());
330 let mapping_green = mapping.0.green();
331 let children: Vec<_> = mapping_green.children().collect();
332
333 let end_index = if let Some(rowan::NodeOrToken::Token(t)) = children.last() {
335 if t.kind() == SyntaxKind::NEWLINE.into() {
336 children.len() - 1
337 } else {
338 children.len()
339 }
340 } else {
341 children.len()
342 };
343
344 for child in &children[..end_index] {
345 match child {
346 rowan::NodeOrToken::Node(n) => {
347 builder.start_node(n.kind());
348 Self::add_green_node_children(&mut builder, n);
349 builder.finish_node();
350 }
351 rowan::NodeOrToken::Token(t) => {
352 builder.token(t.kind(), t.text());
353 }
354 }
355 }
356
357 builder.finish_node(); builder.finish_node(); Document(SyntaxNode::new_root_mut(builder.finish()))
362 }
363
364 fn add_green_node_children(builder: &mut GreenNodeBuilder, node: &rowan::GreenNodeData) {
366 for child in node.children() {
367 match child {
368 rowan::NodeOrToken::Node(n) => {
369 builder.start_node(n.kind());
370 Self::add_green_node_children(builder, n);
371 builder.finish_node();
372 }
373 rowan::NodeOrToken::Token(t) => {
374 builder.token(t.kind(), t.text());
375 }
376 }
377 }
378 }
379
380 pub fn insert_after(
389 &self,
390 after_key: impl crate::AsYaml,
391 key: impl crate::AsYaml,
392 value: impl crate::AsYaml,
393 ) -> bool {
394 self.as_mapping()
395 .is_some_and(|mapping| mapping.insert_after(after_key, key, value))
396 }
397
398 pub fn move_after(
407 &self,
408 after_key: impl crate::AsYaml,
409 key: impl crate::AsYaml,
410 value: impl crate::AsYaml,
411 ) -> bool {
412 self.as_mapping()
413 .is_some_and(|mapping| mapping.move_after(after_key, key, value))
414 }
415
416 pub fn insert_before(
425 &self,
426 before_key: impl crate::AsYaml,
427 key: impl crate::AsYaml,
428 value: impl crate::AsYaml,
429 ) -> bool {
430 self.as_mapping()
431 .is_some_and(|mapping| mapping.insert_before(before_key, key, value))
432 }
433
434 pub fn move_before(
443 &self,
444 before_key: impl crate::AsYaml,
445 key: impl crate::AsYaml,
446 value: impl crate::AsYaml,
447 ) -> bool {
448 self.as_mapping()
449 .is_some_and(|mapping| mapping.move_before(before_key, key, value))
450 }
451
452 pub(crate) fn build_value_content(
454 builder: &mut GreenNodeBuilder,
455 value: impl crate::AsYaml,
456 indent: usize,
457 ) {
458 builder.start_node(SyntaxKind::VALUE.into());
459 value.build_content(builder, indent, false);
460 builder.finish_node(); }
462
463 pub fn insert_at_index(
470 &self,
471 index: usize,
472 key: impl crate::AsYaml,
473 value: impl crate::AsYaml,
474 ) {
475 if let Some(mapping) = self.as_mapping() {
477 mapping.insert_at_index(index, key, value);
478 return;
479 }
480
481 if self.root_node().is_some() {
484 return;
485 }
486
487 let mapping = Mapping::new();
489 mapping.insert_at_index_preserving(index, key, value);
490 let new_doc = Self::from_mapping(mapping);
491 let new_children: Vec<_> = new_doc.0.children_with_tokens().collect();
492 let child_count = self.0.children_with_tokens().count();
493 self.0.splice_children(0..child_count, new_children);
494 }
495
496 pub fn get_string(&self, key: impl crate::AsYaml) -> Option<String> {
504 let content = self.get_node(key)?;
507 if let Some(tagged_node) = TaggedNode::cast(content.clone()) {
508 tagged_node.value().map(|s| s.as_string())
509 } else {
510 Scalar::cast(content).map(|s| s.as_string())
512 }
513 }
514
515 pub fn len(&self) -> usize {
519 self.as_mapping().map_or(0, |m| m.len())
520 }
521
522 pub fn get_mapping(&self, key: impl crate::AsYaml) -> Option<Mapping> {
526 self.get(key).and_then(|n| n.as_mapping().cloned())
527 }
528
529 pub fn get_sequence(&self, key: impl crate::AsYaml) -> Option<Sequence> {
533 self.get(key).and_then(|n| n.as_sequence().cloned())
534 }
535
536 pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
541 self.as_mapping()
542 .is_some_and(|m| m.rename_key(old_key, new_key))
543 }
544
545 pub fn is_sequence(&self, key: impl crate::AsYaml) -> bool {
547 self.get(key)
548 .is_some_and(|node| node.as_sequence().is_some())
549 }
550
551 pub fn reorder_fields<I, K>(&self, order: I)
556 where
557 I: IntoIterator<Item = K>,
558 K: crate::AsYaml,
559 {
560 if let Some(mapping) = self.as_mapping() {
561 mapping.reorder_fields(order);
562 }
563 }
564
565 pub fn validate_schema(
593 &self,
594 validator: &crate::schema::SchemaValidator,
595 ) -> crate::schema::ValidationResult<()> {
596 validator.validate(self)
597 }
598
599 pub fn byte_range(&self) -> crate::TextPosition {
615 self.0.text_range().into()
616 }
617
618 pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
640 let range = self.byte_range();
641 crate::byte_offset_to_line_column(source_text, range.start as usize)
642 }
643
644 pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
653 let range = self.byte_range();
654 crate::byte_offset_to_line_column(source_text, range.end as usize)
655 }
656
657 pub fn comments(&self) -> impl Iterator<Item = super::Comment> {
669 super::comment::comments(&self.0)
670 }
671}
672
673impl Default for Document {
674 fn default() -> Self {
675 Self::new()
676 }
677}
678
679impl std::str::FromStr for Document {
680 type Err = crate::error::YamlError;
681
682 fn from_str(s: &str) -> Result<Self, Self::Err> {
704 let parsed = YamlFile::parse(s);
705
706 if !parsed.positioned_errors().is_empty() {
707 let first_error = &parsed.positioned_errors()[0];
708 let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
709 return Err(crate::error::YamlError::Parse {
710 message: first_error.message.clone(),
711 line: Some(lc.line),
712 column: Some(lc.column),
713 });
714 }
715
716 let tree = parsed.tree();
717 let mut docs = tree.documents();
718 let first = docs.next().unwrap_or_default();
719
720 if docs.next().is_some() {
721 return Err(crate::error::YamlError::InvalidOperation {
722 operation: "Document::from_str".to_string(),
723 reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
724 });
725 }
726
727 for child in tree.0.children_with_tokens() {
733 match child {
734 rowan::NodeOrToken::Token(t) if t.kind() == SyntaxKind::COMMENT => {
735 return Err(crate::error::YamlError::InvalidOperation {
736 operation: "Document::from_str".to_string(),
737 reason: "Input contains stream-level comments outside the document. Use YamlFile::from_str() to preserve them.".to_string(),
738 });
739 }
740 rowan::NodeOrToken::Node(n) if n.kind() == SyntaxKind::DIRECTIVE => {
741 return Err(crate::error::YamlError::InvalidOperation {
742 operation: "Document::from_str".to_string(),
743 reason: "Input contains YAML directives outside the document. Use YamlFile::from_str() to preserve them.".to_string(),
744 });
745 }
746 rowan::NodeOrToken::Node(n) if n.kind() == SyntaxKind::ERROR => {
751 return Err(crate::error::YamlError::Parse {
752 message: format!(
753 "could not be parsed as part of the document: {:?}",
754 n.text().to_string()
755 ),
756 line: Some(
757 crate::byte_offset_to_line_column(
758 s,
759 usize::from(n.text_range().start()),
760 )
761 .line,
762 ),
763 column: Some(
764 crate::byte_offset_to_line_column(
765 s,
766 usize::from(n.text_range().start()),
767 )
768 .column,
769 ),
770 });
771 }
772 _ => {}
773 }
774 }
775
776 Ok(first)
777 }
778}
779
780impl AsYaml for Document {
781 fn as_node(&self) -> Option<&SyntaxNode> {
782 Some(&self.0)
783 }
784
785 fn kind(&self) -> YamlKind {
786 YamlKind::Document
787 }
788
789 fn build_content(
790 &self,
791 builder: &mut rowan::GreenNodeBuilder,
792 _indent: usize,
793 _flow_context: bool,
794 ) -> bool {
795 crate::as_yaml::copy_node_content(builder, &self.0);
796 self.0
797 .last_token()
798 .is_some_and(|t| t.kind() == SyntaxKind::NEWLINE)
799 }
800
801 fn is_inline(&self) -> bool {
802 false
804 }
805}
806#[cfg(test)]
807mod tests {
808 use super::*;
809 use crate::builder::{MappingBuilder, SequenceBuilder};
810 use crate::yaml::YamlFile;
811 use std::str::FromStr;
812
813 #[test]
814 fn from_str_rejects_stranded_content() {
815 for src in ["a\nx: 1\n", "- a\nx: 1\n"] {
822 let err = Document::from_str(src).expect_err("stranded content must not be dropped");
823 let message = err.to_string();
824 assert!(
825 message.contains("could not be parsed"),
826 "{src:?}: unexpected error {message}"
827 );
828 }
829
830 for src in [
834 "a: 1\n",
835 "a\nJ",
836 "- a\n- b\n",
837 "key: value",
838 "&b\nJ",
839 "&b\nx: 1\n",
840 "&b\n- a\n",
841 ] {
842 let doc = Document::from_str(src).expect("valid document");
843 assert_eq!(doc.to_string(), src, "{src:?}");
844 }
845 }
846
847 #[test]
848 fn test_document_stream_features() {
849 let yaml1 = "---\ndoc1: first\n---\ndoc2: second\n...\n";
851 let parsed1 = YamlFile::from_str(yaml1).unwrap();
852 assert_eq!(parsed1.documents().count(), 2);
853 assert_eq!(parsed1.to_string(), yaml1);
854
855 let yaml2 = "---\nkey: value\n...\n";
857 let parsed2 = YamlFile::from_str(yaml2).unwrap();
858 assert_eq!(parsed2.documents().count(), 1);
859 assert_eq!(parsed2.to_string(), yaml2);
860
861 let yaml3 = "key: value\n...\n";
863 let parsed3 = YamlFile::from_str(yaml3).unwrap();
864 assert_eq!(parsed3.documents().count(), 1);
865 assert_eq!(parsed3.to_string(), yaml3);
866 }
867 #[test]
868 fn test_document_level_directives() {
869 let yaml = "%YAML 1.2\n%TAG ! tag:example.com,2000:app/\n---\nfirst: doc\n...\n%YAML 1.2\n---\nsecond: doc\n...\n";
871 let parsed = YamlFile::from_str(yaml).unwrap();
872 assert_eq!(parsed.documents().count(), 2);
873 assert_eq!(parsed.to_string(), yaml);
874 }
875 #[test]
876 fn test_document_schema_validation_api() {
877 let json_yaml = r#"
881name: "John"
882age: 30
883active: true
884items:
885 - "apple"
886 - 42
887 - true
888"#;
889 let doc = YamlFile::from_str(json_yaml).unwrap().document().unwrap();
890
891 assert!(
893 crate::schema::SchemaValidator::json()
894 .validate(&doc)
895 .is_ok(),
896 "JSON-compatible document should pass JSON validation"
897 );
898
899 assert!(
901 crate::schema::SchemaValidator::core()
902 .validate(&doc)
903 .is_ok(),
904 "Valid document should pass Core validation"
905 );
906
907 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
910 assert!(
911 doc.validate_schema(&failsafe_strict).is_err(),
912 "Document with numbers and booleans should fail strict Failsafe validation"
913 );
914
915 let yaml_specific = r#"
917name: "Test"
918created: 2023-12-25T10:30:45Z
919pattern: !!regex '\d+'
920data: !!binary "SGVsbG8="
921"#;
922 let yaml_doc = YamlFile::from_str(yaml_specific)
923 .unwrap()
924 .document()
925 .unwrap();
926
927 assert!(
929 crate::schema::SchemaValidator::core()
930 .validate(&yaml_doc)
931 .is_ok(),
932 "YAML-specific types should pass Core validation"
933 );
934
935 assert!(
937 crate::schema::SchemaValidator::json()
938 .validate(&yaml_doc)
939 .is_err(),
940 "YAML-specific types should fail JSON validation"
941 );
942
943 assert!(
945 crate::schema::SchemaValidator::failsafe()
946 .validate(&yaml_doc)
947 .is_err(),
948 "YAML-specific types should fail Failsafe validation"
949 );
950
951 let string_only = r#"
953name: hello
954message: world
955items:
956 - apple
957 - banana
958nested:
959 key: value
960"#;
961 let str_doc = YamlFile::from_str(string_only).unwrap().document().unwrap();
962
963 assert!(
965 crate::schema::SchemaValidator::failsafe()
966 .validate(&str_doc)
967 .is_ok(),
968 "String-only document should pass Failsafe validation"
969 );
970 assert!(
971 crate::schema::SchemaValidator::json()
972 .validate(&str_doc)
973 .is_ok(),
974 "String-only document should pass JSON validation"
975 );
976 assert!(
977 crate::schema::SchemaValidator::core()
978 .validate(&str_doc)
979 .is_ok(),
980 "String-only document should pass Core validation"
981 );
982 }
983 #[test]
984 fn test_document_set_preserves_position() {
985 let yaml = r#"Name: original
987Version: 1.0
988Author: Someone
989"#;
990 let parsed = YamlFile::from_str(yaml).unwrap();
991 let doc = parsed.document().expect("Should have a document");
992
993 doc.set("Version", 2.0);
995
996 let output = doc.to_string();
997 let expected = r#"Name: original
998Version: 2.0
999Author: Someone
1000"#;
1001 assert_eq!(output, expected);
1002 }
1003 #[test]
1004 fn test_document_schema_coercion_api() {
1005 let coercion_yaml = r#"
1007count: "42"
1008enabled: "true"
1009rate: "3.14"
1010items:
1011 - "100"
1012 - "false"
1013"#;
1014 let doc = YamlFile::from_str(coercion_yaml)
1015 .unwrap()
1016 .document()
1017 .unwrap();
1018 let json_validator = crate::schema::SchemaValidator::json();
1019
1020 assert!(
1022 json_validator.can_coerce(&doc).is_ok(),
1023 "Strings that look like numbers/booleans should be coercible to JSON types"
1024 );
1025
1026 let non_coercible = r#"
1028timestamp: !!timestamp "2023-01-01"
1029pattern: !!regex '\d+'
1030"#;
1031 let non_coer_doc = YamlFile::from_str(non_coercible)
1032 .unwrap()
1033 .document()
1034 .unwrap();
1035
1036 assert!(
1038 json_validator.can_coerce(&non_coer_doc).is_err(),
1039 "YAML-specific types should not be coercible to JSON schema"
1040 );
1041 }
1042 #[test]
1043 fn test_document_schema_validation_errors() {
1044 let nested_yaml = r#"
1046users:
1047 - name: "Alice"
1048 age: 25
1049 metadata:
1050 created: !!timestamp "2023-01-01"
1051 active: true
1052 - name: "Bob"
1053 score: 95.5
1054"#;
1055 let doc = YamlFile::from_str(nested_yaml).unwrap().document().unwrap();
1056
1057 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
1059 let failsafe_result = doc.validate_schema(&failsafe_strict);
1060 assert!(
1061 failsafe_result.is_err(),
1062 "Nested document with numbers should fail strict Failsafe validation"
1063 );
1064
1065 let errors = failsafe_result.unwrap_err();
1066 assert!(!errors.is_empty());
1067
1068 for error in &errors {
1070 assert!(!error.path.is_empty(), "Error should have path: {}", error);
1071 }
1072
1073 let json_result = crate::schema::SchemaValidator::json().validate(&doc);
1075 assert!(
1076 json_result.is_err(),
1077 "Document with timestamp should fail JSON validation"
1078 );
1079
1080 let json_errors = json_result.unwrap_err();
1081 assert!(!json_errors.is_empty());
1082 assert!(
1084 json_errors.iter().any(|e| e.schema_name == "json"),
1085 "Should have JSON schema validation error"
1086 );
1087 }
1088 #[test]
1089 fn test_document_schema_validation_with_custom_validator() {
1090 let yaml = r#"
1092name: "HelloWorld"
1093count: 42
1094active: true
1095"#;
1096 let doc = YamlFile::from_str(yaml).unwrap().document().unwrap();
1097
1098 let json_validator = crate::schema::SchemaValidator::json();
1100 let core_validator = crate::schema::SchemaValidator::core();
1101
1102 assert!(
1104 doc.validate_schema(&core_validator).is_ok(),
1105 "Should pass Core validation"
1106 );
1107 assert!(
1108 doc.validate_schema(&json_validator).is_ok(),
1109 "Should pass JSON validation"
1110 );
1111 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
1113 assert!(
1114 doc.validate_schema(&failsafe_strict).is_err(),
1115 "Should fail strict Failsafe validation"
1116 );
1117
1118 let strict_json = crate::schema::SchemaValidator::json().strict();
1120 assert!(
1122 doc.validate_schema(&strict_json).is_ok(),
1123 "Should pass strict JSON validation (integers and booleans are JSON-compatible)"
1124 );
1125
1126 let strict_failsafe = crate::schema::SchemaValidator::failsafe().strict();
1127 assert!(
1128 doc.validate_schema(&strict_failsafe).is_err(),
1129 "Should fail strict Failsafe validation"
1130 );
1131 }
1132 #[test]
1133 fn test_document_level_insertion_with_complex_types() {
1134 let doc1 = Document::new();
1138 doc1.set("name", "project");
1139 let features = SequenceBuilder::new()
1140 .item("auth")
1141 .item("api")
1142 .item("web")
1143 .build_document()
1144 .as_sequence()
1145 .unwrap();
1146 let success = doc1.insert_after("name", "features", features);
1147 assert!(success);
1148 let output1 = doc1.to_string();
1149 assert_eq!(
1150 output1,
1151 "---\nname: project\nfeatures:\n - auth\n - api\n - web\n"
1152 );
1153
1154 let doc2 = Document::new();
1156 doc2.set("name", "project");
1157 doc2.set("version", "1.0.0");
1158 let database = MappingBuilder::new()
1159 .pair("host", "localhost")
1160 .pair("port", 5432)
1161 .build_document()
1162 .as_mapping()
1163 .unwrap();
1164 let success = doc2.insert_before("version", "database", database);
1165 assert!(success);
1166 let output2 = doc2.to_string();
1167 assert_eq!(
1168 output2,
1169 "---\nname: project\ndatabase:\n host: localhost\n port: 5432\nversion: 1.0.0\n"
1170 );
1171
1172 let doc3 = Document::new();
1174 doc3.set("name", "project");
1175 #[allow(clippy::disallowed_types)]
1177 let tag_set = {
1178 use crate::value::YamlValue;
1179 let mut tags = std::collections::BTreeSet::new();
1180 tags.insert("production".to_string());
1181 tags.insert("database".to_string());
1182 YamlValue::from_set(tags)
1183 };
1184 doc3.insert_at_index(1, "tags", tag_set);
1185 let output3 = doc3.to_string();
1186 assert_eq!(
1187 output3,
1188 "---\nname: project\ntags: !!set\n database: null\n production: null\n"
1189 );
1190
1191 assert!(
1193 YamlFile::from_str(&output1).is_ok(),
1194 "Sequence output should be valid YAML"
1195 );
1196 assert!(
1197 YamlFile::from_str(&output2).is_ok(),
1198 "Mapping output should be valid YAML"
1199 );
1200 assert!(
1201 YamlFile::from_str(&output3).is_ok(),
1202 "Set output should be valid YAML"
1203 );
1204 }
1205
1206 #[test]
1207 fn test_insert_at_index_leaves_sequence_root() {
1208 let doc = Document::from_str("- a\n- b\n").unwrap();
1209 assert!(!doc.set("k", "v"));
1210 assert_eq!(doc.to_string(), "- a\n- b\n");
1211 doc.insert_at_index(0, "k", "v");
1212 assert_eq!(doc.to_string(), "- a\n- b\n");
1213 assert!(doc.as_sequence().is_some());
1214 }
1215
1216 #[test]
1217 fn test_document_api_usage() -> crate::error::YamlResult<()> {
1218 let doc = Document::new();
1220
1221 assert!(!doc.contains_key("Repository"));
1223 doc.set("Repository", "https://github.com/user/repo.git");
1224 assert!(doc.contains_key("Repository"));
1225
1226 assert_eq!(
1228 doc.get_string("Repository"),
1229 Some("https://github.com/user/repo.git".to_string())
1230 );
1231
1232 assert!(!doc.is_empty());
1234
1235 let keys: Vec<_> = doc.keys().collect();
1237 assert_eq!(keys.len(), 1);
1238
1239 assert!(doc.remove("Repository").is_some());
1241 assert!(!doc.contains_key("Repository"));
1242 assert!(doc.is_empty());
1243
1244 Ok(())
1245 }
1246
1247 #[test]
1248 fn test_field_ordering() {
1249 let doc = Document::new();
1250
1251 doc.set("Repository-Browse", "https://github.com/user/repo");
1253 doc.set("Name", "MyProject");
1254 doc.set("Bug-Database", "https://github.com/user/repo/issues");
1255 doc.set("Repository", "https://github.com/user/repo.git");
1256
1257 doc.reorder_fields(["Name", "Bug-Database", "Repository", "Repository-Browse"]);
1259
1260 let keys: Vec<_> = doc.keys().collect();
1262 assert_eq!(keys.len(), 4);
1263 assert_eq!(
1264 keys[0].as_scalar().map(|s| s.as_string()),
1265 Some("Name".to_string())
1266 );
1267 assert_eq!(
1268 keys[1].as_scalar().map(|s| s.as_string()),
1269 Some("Bug-Database".to_string())
1270 );
1271 assert_eq!(
1272 keys[2].as_scalar().map(|s| s.as_string()),
1273 Some("Repository".to_string())
1274 );
1275 assert_eq!(
1276 keys[3].as_scalar().map(|s| s.as_string()),
1277 Some("Repository-Browse".to_string())
1278 );
1279 }
1280
1281 #[test]
1282 fn test_array_detection() {
1283 use crate::scalar::ScalarValue;
1284
1285 let doc = Document::new();
1287
1288 let array_value = SequenceBuilder::new()
1290 .item(ScalarValue::string("https://github.com/user/repo.git"))
1291 .item(ScalarValue::string("https://gitlab.com/user/repo.git"))
1292 .build_document()
1293 .as_sequence()
1294 .unwrap();
1295 doc.set("Repository", &array_value);
1296
1297 assert!(doc.is_sequence("Repository"));
1299 assert!(doc.get_sequence("Repository").is_some());
1302 }
1303
1304 #[test]
1305 fn test_file_io() -> crate::error::YamlResult<()> {
1306 use std::fs;
1307
1308 let test_path = "/tmp/test_yaml_edit.yaml";
1310
1311 let doc = Document::new();
1313 doc.set("Name", "TestProject");
1314 doc.set("Repository", "https://example.com/repo.git");
1315
1316 doc.to_file(test_path)?;
1317
1318 let loaded_doc = Document::from_file(test_path)?;
1320
1321 assert_eq!(
1322 loaded_doc.get_string("Name"),
1323 Some("TestProject".to_string())
1324 );
1325 assert_eq!(
1326 loaded_doc.get_string("Repository"),
1327 Some("https://example.com/repo.git".to_string())
1328 );
1329
1330 let _ = fs::remove_file(test_path);
1332
1333 Ok(())
1334 }
1335
1336 #[test]
1337 fn test_document_from_str_single_document() {
1338 let yaml = "key: value\nport: 8080";
1340 let doc = Document::from_str(yaml).unwrap();
1341
1342 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1343 assert!(doc.contains_key("port"));
1344 }
1345
1346 #[test]
1347 fn test_document_from_str_multiple_documents_error() {
1348 let yaml = "---\nkey: value\n---\nother: data";
1350 let result = Document::from_str(yaml);
1351
1352 assert!(result.is_err());
1353 let err = result.unwrap_err();
1354 match err {
1355 crate::error::YamlError::InvalidOperation { operation, reason } => {
1356 assert_eq!(operation, "Document::from_str");
1357 assert_eq!(
1358 reason,
1359 "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML."
1360 );
1361 }
1362 _ => panic!("Expected InvalidOperation error, got {:?}", err),
1363 }
1364 }
1365
1366 #[test]
1367 fn test_document_from_str_empty() {
1368 let yaml = "";
1370 let doc = Document::from_str(yaml).unwrap();
1371
1372 assert!(doc.is_empty());
1374 }
1375
1376 #[test]
1377 fn test_document_from_str_bare_document() {
1378 let yaml = "name: test\nversion: 1.0";
1380 let doc = Document::from_str(yaml).unwrap();
1381
1382 assert_eq!(doc.get_string("name"), Some("test".to_string()));
1383 assert_eq!(doc.get_string("version"), Some("1.0".to_string()));
1384 }
1385
1386 #[test]
1387 fn test_document_from_str_with_explicit_marker() {
1388 let yaml = "---\nkey: value";
1390 let doc = Document::from_str(yaml).unwrap();
1391
1392 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1393 }
1394
1395 #[test]
1396 fn test_document_from_str_complex_structure() {
1397 let yaml = r#"
1399database:
1400 host: localhost
1401 port: 5432
1402 credentials:
1403 username: admin
1404 password: secret
1405features:
1406 - auth
1407 - logging
1408 - metrics
1409"#;
1410 let doc = Document::from_str(yaml).unwrap();
1411
1412 assert!(doc.contains_key("database"));
1414 assert!(doc.contains_key("features"));
1415
1416 let db = doc.get("database").unwrap();
1418 if let Some(db_map) = db.as_mapping() {
1419 assert!(db_map.contains_key("host"));
1420 } else {
1421 panic!("Expected mapping for database");
1422 }
1423 }
1424
1425 #[test]
1426 fn test_is_sequence_block() {
1427 let doc = Document::from_str("tags:\n - alpha\n - beta\n - gamma").unwrap();
1428 assert!(doc.is_sequence("tags"));
1429 assert_eq!(
1430 doc.get_sequence("tags")
1431 .and_then(|s| s.get(0))
1432 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1433 Some("alpha".to_string())
1434 );
1435 }
1436
1437 #[test]
1438 fn test_is_sequence_flow() {
1439 let doc = Document::from_str("tags: [alpha, beta, gamma]").unwrap();
1440 assert!(doc.is_sequence("tags"));
1441 assert_eq!(
1442 doc.get_sequence("tags")
1443 .and_then(|s| s.get(0))
1444 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1445 Some("alpha".to_string())
1446 );
1447 }
1448
1449 #[test]
1450 fn test_sequence_first_element_flow_quoted_with_comma() {
1451 let doc = Document::from_str("tags: [\"hello, world\", beta]").unwrap();
1453 assert_eq!(
1454 doc.get_sequence("tags")
1455 .and_then(|s| s.get(0))
1456 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1457 Some("hello, world".to_string())
1458 );
1459 }
1460
1461 #[test]
1462 fn test_is_sequence_missing_key() {
1463 let doc = Document::from_str("name: test").unwrap();
1464 assert!(!doc.is_sequence("tags"));
1465 }
1466
1467 #[test]
1468 fn test_is_sequence_not_sequence() {
1469 let doc = Document::from_str("name: test").unwrap();
1470 assert!(!doc.is_sequence("name"));
1471 }
1472
1473 #[test]
1474 fn test_is_sequence_empty_sequence() {
1475 let doc = Document::from_str("tags: []").unwrap();
1476 assert!(doc.is_sequence("tags"));
1477 assert_eq!(doc.get_sequence("tags").map(|s| s.len()), Some(0));
1478 }
1479
1480 #[test]
1481 fn test_get_string_plain_scalar() {
1482 let doc = Document::from_str("key: hello").unwrap();
1483 assert_eq!(doc.get_string("key"), Some("hello".to_string()));
1484 }
1485
1486 #[test]
1487 fn test_get_string_double_quoted_with_escapes() {
1488 let doc = Document::from_str(r#"key: "hello \"world\"""#).unwrap();
1489 assert_eq!(doc.get_string("key"), Some(r#"hello "world""#.to_string()));
1490 }
1491
1492 #[test]
1493 fn test_get_string_single_quoted() {
1494 let doc = Document::from_str("key: 'it''s fine'").unwrap();
1495 assert_eq!(doc.get_string("key"), Some("it's fine".to_string()));
1496 }
1497
1498 #[test]
1499 fn test_get_string_missing_key() {
1500 let doc = Document::from_str("other: value").unwrap();
1501 assert_eq!(doc.get_string("key"), None);
1502 }
1503
1504 #[test]
1505 fn test_get_string_sequence_value_returns_none() {
1506 let doc = Document::from_str("key:\n - a\n - b").unwrap();
1507 assert_eq!(doc.get_string("key"), None);
1508 }
1509
1510 #[test]
1511 fn test_get_string_mapping_value_returns_none() {
1512 let doc = Document::from_str("key:\n nested: value").unwrap();
1513 assert_eq!(doc.get_string("key"), None);
1514 }
1515
1516 #[test]
1517 fn test_insert_after_preserves_newline() {
1518 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new\n";
1520 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1521
1522 if let Some(doc) = yaml_obj.document() {
1524 let result = doc.insert_after(
1525 "Bug-Submit",
1526 "Repository",
1527 "https://github.com/example/example.git",
1528 );
1529 assert!(result, "insert_after should return true when key is found");
1530
1531 let output = doc.to_string();
1533
1534 let expected = "---
1535Bug-Database: https://github.com/example/example/issues
1536Bug-Submit: https://github.com/example/example/issues/new
1537Repository: https://github.com/example/example.git
1538";
1539 assert_eq!(output, expected);
1540 }
1541 }
1542
1543 #[test]
1544 fn test_insert_after_without_trailing_newline() {
1545 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new";
1547 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1548
1549 if let Some(doc) = yaml_obj.document() {
1550 let result = doc.insert_after(
1551 "Bug-Submit",
1552 "Repository",
1553 "https://github.com/example/example.git",
1554 );
1555 assert!(result, "insert_after should return true when key is found");
1556
1557 let output = doc.to_string();
1558
1559 let expected = "---
1560Bug-Database: https://github.com/example/example/issues
1561Bug-Submit: https://github.com/example/example/issues/new
1562Repository: https://github.com/example/example.git
1563";
1564 assert_eq!(output, expected);
1565 }
1566 }
1567
1568 #[test]
1569 fn test_document_from_str_leading_comment_errors() {
1570 let yaml = "# leading comment\nkey: value\n";
1574 let err = Document::from_str(yaml).unwrap_err();
1575 match err {
1576 crate::error::YamlError::InvalidOperation { operation, reason } => {
1577 assert_eq!(operation, "Document::from_str");
1578 assert!(
1579 reason.contains("stream-level comments"),
1580 "unexpected reason: {reason}"
1581 );
1582 }
1583 other => panic!("expected InvalidOperation, got {other:?}"),
1584 }
1585 }
1586
1587 #[test]
1588 fn test_document_from_str_directive_errors() {
1589 let yaml = "%YAML 1.2\n---\nkey: value\n";
1592 let err = Document::from_str(yaml).unwrap_err();
1593 match err {
1594 crate::error::YamlError::InvalidOperation { operation, reason } => {
1595 assert_eq!(operation, "Document::from_str");
1596 assert!(reason.contains("directives"), "unexpected reason: {reason}");
1597 }
1598 other => panic!("expected InvalidOperation, got {other:?}"),
1599 }
1600 }
1601
1602 #[test]
1603 fn test_document_from_str_trailing_comment_errors() {
1604 let yaml = "key: value\n...\n# trailing\n";
1606 assert!(matches!(
1607 Document::from_str(yaml),
1608 Err(crate::error::YamlError::InvalidOperation { .. })
1609 ));
1610 }
1611
1612 #[test]
1613 fn test_document_from_str_ignoring_comments_drops_prefix() {
1614 let yaml = "# leading\n%YAML 1.2\n---\nkey: value\n";
1615 let doc = Document::from_str_ignoring_comments(yaml).unwrap();
1616 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1617 assert_eq!(doc.to_string(), "---\nkey: value\n");
1620 }
1621
1622 #[test]
1623 fn test_document_from_str_ignoring_comments_rejects_multi_document() {
1624 let yaml = "---\na: 1\n---\nb: 2\n";
1625 assert!(matches!(
1626 Document::from_str_ignoring_comments(yaml),
1627 Err(crate::error::YamlError::InvalidOperation { .. })
1628 ));
1629 }
1630
1631 #[test]
1632 fn test_document_from_str_interior_comment_ok() {
1633 let yaml = "key: value # trailing\n";
1636 let doc = Document::from_str(yaml).unwrap();
1637 assert_eq!(doc.to_string(), yaml);
1638 }
1639
1640 #[test]
1641 fn test_from_str_relaxed_valid() {
1642 let (doc, errors) = Document::from_str_relaxed("key: value\n");
1643 assert!(errors.is_empty());
1644 let mapping = doc.as_mapping().unwrap();
1645 assert_eq!(
1646 mapping.get("key").unwrap().as_scalar().unwrap().to_string(),
1647 "value"
1648 );
1649 }
1650
1651 #[test]
1652 fn test_from_str_relaxed_with_errors() {
1653 let (doc, errors) = Document::from_str_relaxed("key: [unclosed");
1654 assert!(!errors.is_empty());
1655 assert!(doc.as_mapping().is_some());
1657 }
1658
1659 #[test]
1660 fn test_from_str_relaxed_multi_document() {
1661 let (doc, errors) = Document::from_str_relaxed("a: 1\n---\nb: 2\n");
1663 assert!(errors.is_empty());
1664 let mapping = doc.as_mapping().unwrap();
1665 assert_eq!(
1666 mapping.get("a").unwrap().as_scalar().unwrap().to_string(),
1667 "1"
1668 );
1669 }
1670
1671 #[test]
1672 fn test_from_str_relaxed_empty_input() {
1673 let (doc, errors) = Document::from_str_relaxed("");
1674 assert!(errors.is_empty());
1675 assert!(doc.as_mapping().is_none());
1677 }
1678
1679 #[test]
1680 fn test_set_on_sequence_document_does_not_append_mapping() {
1681 let doc = Document::from_str("- a\n- b\n").unwrap();
1682 assert!(!doc.set("k", "v"));
1683 assert_eq!(doc.to_string(), "- a\n- b\n");
1684 assert!(doc.get("k").is_none());
1685 assert!(doc.as_sequence().is_some());
1686 assert!(doc.as_mapping().is_none());
1687
1688 let empty = Document::new();
1689 assert!(empty.set("k", "v"));
1690 assert_eq!(empty.get_string("k"), Some("v".to_string()));
1691 }
1692}