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 pub fn as_mapping(&self) -> Option<Mapping> {
167 self.root_node().and_then(Mapping::cast)
168 }
169
170 pub fn as_sequence(&self) -> Option<Sequence> {
172 self.root_node().and_then(Sequence::cast)
173 }
174
175 pub fn as_scalar(&self) -> Option<Scalar> {
177 self.root_node().and_then(Scalar::cast)
178 }
179
180 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
182 self.as_mapping().is_some_and(|m| m.contains_key(key))
183 }
184
185 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
189 self.as_mapping().and_then(|m| m.get(key))
190 }
191
192 pub(crate) fn get_node(&self, key: impl crate::AsYaml) -> Option<SyntaxNode> {
197 self.as_mapping().and_then(|m| m.get_node(key))
198 }
199
200 pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) {
202 if let Some(mapping) = self.as_mapping() {
203 mapping.set(key, value);
204 } else {
206 let mapping = Mapping::new();
208 mapping.set(key, value);
209
210 let child_count = self.0.children_with_tokens().count();
212 self.0
213 .splice_children(child_count..child_count, vec![mapping.0.into()]);
214 }
215 }
216
217 pub fn set_with_field_order<I, K>(
223 &self,
224 key: impl crate::AsYaml,
225 value: impl crate::AsYaml,
226 field_order: I,
227 ) where
228 I: IntoIterator<Item = K>,
229 K: crate::AsYaml,
230 {
231 let field_order: Vec<K> = field_order.into_iter().collect();
233 if let Some(mapping) = self.as_mapping() {
234 mapping.set_with_field_order(key, value, field_order);
235 } else {
237 let mapping = Mapping::new();
239 mapping.set_with_field_order(key, value, field_order);
240 let child_count = self.0.children_with_tokens().count();
241 self.0
242 .splice_children(child_count..child_count, vec![mapping.0.into()]);
243 }
244 }
245
246 pub fn remove(&self, key: impl crate::AsYaml) -> Option<super::MappingEntry> {
253 self.as_mapping()?.remove(key)
254 }
255
256 pub(crate) fn key_nodes(&self) -> impl Iterator<Item = SyntaxNode> + '_ {
258 self.as_mapping()
259 .into_iter()
260 .flat_map(|m| m.key_nodes().collect::<Vec<_>>())
261 }
262
263 pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
269 self.key_nodes().filter_map(|key_node| {
270 key_node
271 .children()
272 .next()
273 .and_then(crate::as_yaml::YamlNode::from_syntax)
274 })
275 }
276
277 pub fn is_empty(&self) -> bool {
279 self.as_mapping().map_or(true, |m| m.is_empty())
280 }
281
282 pub fn from_mapping(mapping: Mapping) -> Self {
284 let mut builder = GreenNodeBuilder::new();
286 builder.start_node(SyntaxKind::DOCUMENT.into());
287 builder.token(SyntaxKind::DOC_START.into(), "---");
289 builder.token(SyntaxKind::WHITESPACE.into(), "\n");
290
291 builder.start_node(SyntaxKind::MAPPING.into());
293 let mapping_green = mapping.0.green();
294 let children: Vec<_> = mapping_green.children().collect();
295
296 let end_index = if let Some(rowan::NodeOrToken::Token(t)) = children.last() {
298 if t.kind() == SyntaxKind::NEWLINE.into() {
299 children.len() - 1
300 } else {
301 children.len()
302 }
303 } else {
304 children.len()
305 };
306
307 for child in &children[..end_index] {
308 match child {
309 rowan::NodeOrToken::Node(n) => {
310 builder.start_node(n.kind());
311 Self::add_green_node_children(&mut builder, n);
312 builder.finish_node();
313 }
314 rowan::NodeOrToken::Token(t) => {
315 builder.token(t.kind(), t.text());
316 }
317 }
318 }
319
320 builder.finish_node(); builder.finish_node(); Document(SyntaxNode::new_root_mut(builder.finish()))
325 }
326
327 fn add_green_node_children(builder: &mut GreenNodeBuilder, node: &rowan::GreenNodeData) {
329 for child in node.children() {
330 match child {
331 rowan::NodeOrToken::Node(n) => {
332 builder.start_node(n.kind());
333 Self::add_green_node_children(builder, n);
334 builder.finish_node();
335 }
336 rowan::NodeOrToken::Token(t) => {
337 builder.token(t.kind(), t.text());
338 }
339 }
340 }
341 }
342
343 pub fn insert_after(
352 &self,
353 after_key: impl crate::AsYaml,
354 key: impl crate::AsYaml,
355 value: impl crate::AsYaml,
356 ) -> bool {
357 if let Some(mapping) = self.as_mapping() {
358 mapping.insert_after(after_key, key, value)
359 } else {
360 false
361 }
362 }
363
364 pub fn move_after(
373 &self,
374 after_key: impl crate::AsYaml,
375 key: impl crate::AsYaml,
376 value: impl crate::AsYaml,
377 ) -> bool {
378 if let Some(mapping) = self.as_mapping() {
379 mapping.move_after(after_key, key, value)
380 } else {
381 false
382 }
383 }
384
385 pub fn insert_before(
394 &self,
395 before_key: impl crate::AsYaml,
396 key: impl crate::AsYaml,
397 value: impl crate::AsYaml,
398 ) -> bool {
399 if let Some(mapping) = self.as_mapping() {
400 mapping.insert_before(before_key, key, value)
401 } else {
402 false
403 }
404 }
405
406 pub fn move_before(
415 &self,
416 before_key: impl crate::AsYaml,
417 key: impl crate::AsYaml,
418 value: impl crate::AsYaml,
419 ) -> bool {
420 if let Some(mapping) = self.as_mapping() {
421 mapping.move_before(before_key, key, value)
422 } else {
423 false
424 }
425 }
426
427 pub(crate) fn build_value_content(
429 builder: &mut GreenNodeBuilder,
430 value: impl crate::AsYaml,
431 indent: usize,
432 ) {
433 builder.start_node(SyntaxKind::VALUE.into());
434 value.build_content(builder, indent, false);
435 builder.finish_node(); }
437
438 pub fn insert_at_index(
444 &self,
445 index: usize,
446 key: impl crate::AsYaml,
447 value: impl crate::AsYaml,
448 ) {
449 if let Some(mapping) = self.as_mapping() {
451 mapping.insert_at_index(index, key, value);
452 return;
453 }
454
455 let mapping = Mapping::new();
457 mapping.insert_at_index_preserving(index, key, value);
458 let new_doc = Self::from_mapping(mapping);
459 let new_children: Vec<_> = new_doc.0.children_with_tokens().collect();
460 let child_count = self.0.children_with_tokens().count();
461 self.0.splice_children(0..child_count, new_children);
462 }
463
464 pub fn get_string(&self, key: impl crate::AsYaml) -> Option<String> {
472 let content = self.get_node(key)?;
475 if let Some(tagged_node) = TaggedNode::cast(content.clone()) {
476 tagged_node.value().map(|s| s.as_string())
477 } else {
478 Scalar::cast(content).map(|s| s.as_string())
480 }
481 }
482
483 pub fn len(&self) -> usize {
487 self.as_mapping().map(|m| m.len()).unwrap_or(0)
488 }
489
490 pub fn get_mapping(&self, key: impl crate::AsYaml) -> Option<Mapping> {
494 self.get(key).and_then(|n| n.as_mapping().cloned())
495 }
496
497 pub fn get_sequence(&self, key: impl crate::AsYaml) -> Option<Sequence> {
501 self.get(key).and_then(|n| n.as_sequence().cloned())
502 }
503
504 pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
509 self.as_mapping()
510 .map(|m| m.rename_key(old_key, new_key))
511 .unwrap_or(false)
512 }
513
514 pub fn is_sequence(&self, key: impl crate::AsYaml) -> bool {
516 self.get(key)
517 .map(|node| node.as_sequence().is_some())
518 .unwrap_or(false)
519 }
520
521 pub fn reorder_fields<I, K>(&self, order: I)
526 where
527 I: IntoIterator<Item = K>,
528 K: crate::AsYaml,
529 {
530 if let Some(mapping) = self.as_mapping() {
531 mapping.reorder_fields(order);
532 }
533 }
534
535 pub fn validate_schema(
563 &self,
564 validator: &crate::schema::SchemaValidator,
565 ) -> crate::schema::ValidationResult<()> {
566 validator.validate(self)
567 }
568
569 pub fn byte_range(&self) -> crate::TextPosition {
585 self.0.text_range().into()
586 }
587
588 pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
610 let range = self.byte_range();
611 crate::byte_offset_to_line_column(source_text, range.start as usize)
612 }
613
614 pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
623 let range = self.byte_range();
624 crate::byte_offset_to_line_column(source_text, range.end as usize)
625 }
626
627 pub fn comments(&self) -> impl Iterator<Item = super::Comment> {
639 super::comment::comments(&self.0)
640 }
641}
642
643impl Default for Document {
644 fn default() -> Self {
645 Self::new()
646 }
647}
648
649impl std::str::FromStr for Document {
650 type Err = crate::error::YamlError;
651
652 fn from_str(s: &str) -> Result<Self, Self::Err> {
674 let parsed = YamlFile::parse(s);
675
676 if !parsed.positioned_errors().is_empty() {
677 let first_error = &parsed.positioned_errors()[0];
678 let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
679 return Err(crate::error::YamlError::Parse {
680 message: first_error.message.clone(),
681 line: Some(lc.line),
682 column: Some(lc.column),
683 });
684 }
685
686 let tree = parsed.tree();
687 let mut docs = tree.documents();
688 let first = docs.next().unwrap_or_default();
689
690 if docs.next().is_some() {
691 return Err(crate::error::YamlError::InvalidOperation {
692 operation: "Document::from_str".to_string(),
693 reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
694 });
695 }
696
697 for child in tree.0.children_with_tokens() {
703 match child {
704 rowan::NodeOrToken::Token(t) if t.kind() == SyntaxKind::COMMENT => {
705 return Err(crate::error::YamlError::InvalidOperation {
706 operation: "Document::from_str".to_string(),
707 reason: "Input contains stream-level comments outside the document. Use YamlFile::from_str() to preserve them.".to_string(),
708 });
709 }
710 rowan::NodeOrToken::Node(n) if n.kind() == SyntaxKind::DIRECTIVE => {
711 return Err(crate::error::YamlError::InvalidOperation {
712 operation: "Document::from_str".to_string(),
713 reason: "Input contains YAML directives outside the document. Use YamlFile::from_str() to preserve them.".to_string(),
714 });
715 }
716 _ => {}
717 }
718 }
719
720 Ok(first)
721 }
722}
723
724impl AsYaml for Document {
725 fn as_node(&self) -> Option<&SyntaxNode> {
726 Some(&self.0)
727 }
728
729 fn kind(&self) -> YamlKind {
730 YamlKind::Document
731 }
732
733 fn build_content(
734 &self,
735 builder: &mut rowan::GreenNodeBuilder,
736 _indent: usize,
737 _flow_context: bool,
738 ) -> bool {
739 crate::as_yaml::copy_node_content(builder, &self.0);
740 self.0
741 .last_token()
742 .map(|t| t.kind() == SyntaxKind::NEWLINE)
743 .unwrap_or(false)
744 }
745
746 fn is_inline(&self) -> bool {
747 false
749 }
750}
751#[cfg(test)]
752mod tests {
753 use super::*;
754 use crate::builder::{MappingBuilder, SequenceBuilder};
755 use crate::yaml::YamlFile;
756 use std::str::FromStr;
757
758 #[test]
759 fn test_document_stream_features() {
760 let yaml1 = "---\ndoc1: first\n---\ndoc2: second\n...\n";
762 let parsed1 = YamlFile::from_str(yaml1).unwrap();
763 assert_eq!(parsed1.documents().count(), 2);
764 assert_eq!(parsed1.to_string(), yaml1);
765
766 let yaml2 = "---\nkey: value\n...\n";
768 let parsed2 = YamlFile::from_str(yaml2).unwrap();
769 assert_eq!(parsed2.documents().count(), 1);
770 assert_eq!(parsed2.to_string(), yaml2);
771
772 let yaml3 = "key: value\n...\n";
774 let parsed3 = YamlFile::from_str(yaml3).unwrap();
775 assert_eq!(parsed3.documents().count(), 1);
776 assert_eq!(parsed3.to_string(), yaml3);
777 }
778 #[test]
779 fn test_document_level_directives() {
780 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";
782 let parsed = YamlFile::from_str(yaml).unwrap();
783 assert_eq!(parsed.documents().count(), 2);
784 assert_eq!(parsed.to_string(), yaml);
785 }
786 #[test]
787 fn test_document_schema_validation_api() {
788 let json_yaml = r#"
792name: "John"
793age: 30
794active: true
795items:
796 - "apple"
797 - 42
798 - true
799"#;
800 let doc = YamlFile::from_str(json_yaml).unwrap().document().unwrap();
801
802 assert!(
804 crate::schema::SchemaValidator::json()
805 .validate(&doc)
806 .is_ok(),
807 "JSON-compatible document should pass JSON validation"
808 );
809
810 assert!(
812 crate::schema::SchemaValidator::core()
813 .validate(&doc)
814 .is_ok(),
815 "Valid document should pass Core validation"
816 );
817
818 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
821 assert!(
822 doc.validate_schema(&failsafe_strict).is_err(),
823 "Document with numbers and booleans should fail strict Failsafe validation"
824 );
825
826 let yaml_specific = r#"
828name: "Test"
829created: 2023-12-25T10:30:45Z
830pattern: !!regex '\d+'
831data: !!binary "SGVsbG8="
832"#;
833 let yaml_doc = YamlFile::from_str(yaml_specific)
834 .unwrap()
835 .document()
836 .unwrap();
837
838 assert!(
840 crate::schema::SchemaValidator::core()
841 .validate(&yaml_doc)
842 .is_ok(),
843 "YAML-specific types should pass Core validation"
844 );
845
846 assert!(
848 crate::schema::SchemaValidator::json()
849 .validate(&yaml_doc)
850 .is_err(),
851 "YAML-specific types should fail JSON validation"
852 );
853
854 assert!(
856 crate::schema::SchemaValidator::failsafe()
857 .validate(&yaml_doc)
858 .is_err(),
859 "YAML-specific types should fail Failsafe validation"
860 );
861
862 let string_only = r#"
864name: hello
865message: world
866items:
867 - apple
868 - banana
869nested:
870 key: value
871"#;
872 let str_doc = YamlFile::from_str(string_only).unwrap().document().unwrap();
873
874 assert!(
876 crate::schema::SchemaValidator::failsafe()
877 .validate(&str_doc)
878 .is_ok(),
879 "String-only document should pass Failsafe validation"
880 );
881 assert!(
882 crate::schema::SchemaValidator::json()
883 .validate(&str_doc)
884 .is_ok(),
885 "String-only document should pass JSON validation"
886 );
887 assert!(
888 crate::schema::SchemaValidator::core()
889 .validate(&str_doc)
890 .is_ok(),
891 "String-only document should pass Core validation"
892 );
893 }
894 #[test]
895 fn test_document_set_preserves_position() {
896 let yaml = r#"Name: original
898Version: 1.0
899Author: Someone
900"#;
901 let parsed = YamlFile::from_str(yaml).unwrap();
902 let doc = parsed.document().expect("Should have a document");
903
904 doc.set("Version", 2.0);
906
907 let output = doc.to_string();
908 let expected = r#"Name: original
909Version: 2.0
910Author: Someone
911"#;
912 assert_eq!(output, expected);
913 }
914 #[test]
915 fn test_document_schema_coercion_api() {
916 let coercion_yaml = r#"
918count: "42"
919enabled: "true"
920rate: "3.14"
921items:
922 - "100"
923 - "false"
924"#;
925 let doc = YamlFile::from_str(coercion_yaml)
926 .unwrap()
927 .document()
928 .unwrap();
929 let json_validator = crate::schema::SchemaValidator::json();
930
931 assert!(
933 json_validator.can_coerce(&doc).is_ok(),
934 "Strings that look like numbers/booleans should be coercible to JSON types"
935 );
936
937 let non_coercible = r#"
939timestamp: !!timestamp "2023-01-01"
940pattern: !!regex '\d+'
941"#;
942 let non_coer_doc = YamlFile::from_str(non_coercible)
943 .unwrap()
944 .document()
945 .unwrap();
946
947 assert!(
949 json_validator.can_coerce(&non_coer_doc).is_err(),
950 "YAML-specific types should not be coercible to JSON schema"
951 );
952 }
953 #[test]
954 fn test_document_schema_validation_errors() {
955 let nested_yaml = r#"
957users:
958 - name: "Alice"
959 age: 25
960 metadata:
961 created: !!timestamp "2023-01-01"
962 active: true
963 - name: "Bob"
964 score: 95.5
965"#;
966 let doc = YamlFile::from_str(nested_yaml).unwrap().document().unwrap();
967
968 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
970 let failsafe_result = doc.validate_schema(&failsafe_strict);
971 assert!(
972 failsafe_result.is_err(),
973 "Nested document with numbers should fail strict Failsafe validation"
974 );
975
976 let errors = failsafe_result.unwrap_err();
977 assert!(!errors.is_empty());
978
979 for error in &errors {
981 assert!(!error.path.is_empty(), "Error should have path: {}", error);
982 }
983
984 let json_result = crate::schema::SchemaValidator::json().validate(&doc);
986 assert!(
987 json_result.is_err(),
988 "Document with timestamp should fail JSON validation"
989 );
990
991 let json_errors = json_result.unwrap_err();
992 assert!(!json_errors.is_empty());
993 assert!(
995 json_errors.iter().any(|e| e.schema_name == "json"),
996 "Should have JSON schema validation error"
997 );
998 }
999 #[test]
1000 fn test_document_schema_validation_with_custom_validator() {
1001 let yaml = r#"
1003name: "HelloWorld"
1004count: 42
1005active: true
1006"#;
1007 let doc = YamlFile::from_str(yaml).unwrap().document().unwrap();
1008
1009 let json_validator = crate::schema::SchemaValidator::json();
1011 let core_validator = crate::schema::SchemaValidator::core();
1012
1013 assert!(
1015 doc.validate_schema(&core_validator).is_ok(),
1016 "Should pass Core validation"
1017 );
1018 assert!(
1019 doc.validate_schema(&json_validator).is_ok(),
1020 "Should pass JSON validation"
1021 );
1022 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
1024 assert!(
1025 doc.validate_schema(&failsafe_strict).is_err(),
1026 "Should fail strict Failsafe validation"
1027 );
1028
1029 let strict_json = crate::schema::SchemaValidator::json().strict();
1031 assert!(
1033 doc.validate_schema(&strict_json).is_ok(),
1034 "Should pass strict JSON validation (integers and booleans are JSON-compatible)"
1035 );
1036
1037 let strict_failsafe = crate::schema::SchemaValidator::failsafe().strict();
1038 assert!(
1039 doc.validate_schema(&strict_failsafe).is_err(),
1040 "Should fail strict Failsafe validation"
1041 );
1042 }
1043 #[test]
1044 fn test_document_level_insertion_with_complex_types() {
1045 let doc1 = Document::new();
1049 doc1.set("name", "project");
1050 let features = SequenceBuilder::new()
1051 .item("auth")
1052 .item("api")
1053 .item("web")
1054 .build_document()
1055 .as_sequence()
1056 .unwrap();
1057 let success = doc1.insert_after("name", "features", features);
1058 assert!(success);
1059 let output1 = doc1.to_string();
1060 assert_eq!(
1061 output1,
1062 "---\nname: project\nfeatures:\n - auth\n - api\n - web\n"
1063 );
1064
1065 let doc2 = Document::new();
1067 doc2.set("name", "project");
1068 doc2.set("version", "1.0.0");
1069 let database = MappingBuilder::new()
1070 .pair("host", "localhost")
1071 .pair("port", 5432)
1072 .build_document()
1073 .as_mapping()
1074 .unwrap();
1075 let success = doc2.insert_before("version", "database", database);
1076 assert!(success);
1077 let output2 = doc2.to_string();
1078 assert_eq!(
1079 output2,
1080 "---\nname: project\ndatabase:\n host: localhost\n port: 5432\nversion: 1.0.0\n"
1081 );
1082
1083 let doc3 = Document::new();
1085 doc3.set("name", "project");
1086 #[allow(clippy::disallowed_types)]
1088 let tag_set = {
1089 use crate::value::YamlValue;
1090 let mut tags = std::collections::BTreeSet::new();
1091 tags.insert("production".to_string());
1092 tags.insert("database".to_string());
1093 YamlValue::from_set(tags)
1094 };
1095 doc3.insert_at_index(1, "tags", tag_set);
1096 let output3 = doc3.to_string();
1097 assert_eq!(
1098 output3,
1099 "---\nname: project\ntags: !!set\n database: null\n production: null\n"
1100 );
1101
1102 assert!(
1104 YamlFile::from_str(&output1).is_ok(),
1105 "Sequence output should be valid YAML"
1106 );
1107 assert!(
1108 YamlFile::from_str(&output2).is_ok(),
1109 "Mapping output should be valid YAML"
1110 );
1111 assert!(
1112 YamlFile::from_str(&output3).is_ok(),
1113 "Set output should be valid YAML"
1114 );
1115 }
1116
1117 #[test]
1118 fn test_document_api_usage() -> crate::error::YamlResult<()> {
1119 let doc = Document::new();
1121
1122 assert!(!doc.contains_key("Repository"));
1124 doc.set("Repository", "https://github.com/user/repo.git");
1125 assert!(doc.contains_key("Repository"));
1126
1127 assert_eq!(
1129 doc.get_string("Repository"),
1130 Some("https://github.com/user/repo.git".to_string())
1131 );
1132
1133 assert!(!doc.is_empty());
1135
1136 let keys: Vec<_> = doc.keys().collect();
1138 assert_eq!(keys.len(), 1);
1139
1140 assert!(doc.remove("Repository").is_some());
1142 assert!(!doc.contains_key("Repository"));
1143 assert!(doc.is_empty());
1144
1145 Ok(())
1146 }
1147
1148 #[test]
1149 fn test_field_ordering() {
1150 let doc = Document::new();
1151
1152 doc.set("Repository-Browse", "https://github.com/user/repo");
1154 doc.set("Name", "MyProject");
1155 doc.set("Bug-Database", "https://github.com/user/repo/issues");
1156 doc.set("Repository", "https://github.com/user/repo.git");
1157
1158 doc.reorder_fields(["Name", "Bug-Database", "Repository", "Repository-Browse"]);
1160
1161 let keys: Vec<_> = doc.keys().collect();
1163 assert_eq!(keys.len(), 4);
1164 assert_eq!(
1165 keys[0].as_scalar().map(|s| s.as_string()),
1166 Some("Name".to_string())
1167 );
1168 assert_eq!(
1169 keys[1].as_scalar().map(|s| s.as_string()),
1170 Some("Bug-Database".to_string())
1171 );
1172 assert_eq!(
1173 keys[2].as_scalar().map(|s| s.as_string()),
1174 Some("Repository".to_string())
1175 );
1176 assert_eq!(
1177 keys[3].as_scalar().map(|s| s.as_string()),
1178 Some("Repository-Browse".to_string())
1179 );
1180 }
1181
1182 #[test]
1183 fn test_array_detection() {
1184 use crate::scalar::ScalarValue;
1185
1186 let doc = Document::new();
1188
1189 let array_value = SequenceBuilder::new()
1191 .item(ScalarValue::string("https://github.com/user/repo.git"))
1192 .item(ScalarValue::string("https://gitlab.com/user/repo.git"))
1193 .build_document()
1194 .as_sequence()
1195 .unwrap();
1196 doc.set("Repository", &array_value);
1197
1198 assert!(doc.is_sequence("Repository"));
1200 assert!(doc.get_sequence("Repository").is_some());
1203 }
1204
1205 #[test]
1206 fn test_file_io() -> crate::error::YamlResult<()> {
1207 use std::fs;
1208
1209 let test_path = "/tmp/test_yaml_edit.yaml";
1211
1212 let doc = Document::new();
1214 doc.set("Name", "TestProject");
1215 doc.set("Repository", "https://example.com/repo.git");
1216
1217 doc.to_file(test_path)?;
1218
1219 let loaded_doc = Document::from_file(test_path)?;
1221
1222 assert_eq!(
1223 loaded_doc.get_string("Name"),
1224 Some("TestProject".to_string())
1225 );
1226 assert_eq!(
1227 loaded_doc.get_string("Repository"),
1228 Some("https://example.com/repo.git".to_string())
1229 );
1230
1231 let _ = fs::remove_file(test_path);
1233
1234 Ok(())
1235 }
1236
1237 #[test]
1238 fn test_document_from_str_single_document() {
1239 let yaml = "key: value\nport: 8080";
1241 let doc = Document::from_str(yaml).unwrap();
1242
1243 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1244 assert!(doc.contains_key("port"));
1245 }
1246
1247 #[test]
1248 fn test_document_from_str_multiple_documents_error() {
1249 let yaml = "---\nkey: value\n---\nother: data";
1251 let result = Document::from_str(yaml);
1252
1253 assert!(result.is_err());
1254 let err = result.unwrap_err();
1255 match err {
1256 crate::error::YamlError::InvalidOperation { operation, reason } => {
1257 assert_eq!(operation, "Document::from_str");
1258 assert_eq!(
1259 reason,
1260 "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML."
1261 );
1262 }
1263 _ => panic!("Expected InvalidOperation error, got {:?}", err),
1264 }
1265 }
1266
1267 #[test]
1268 fn test_document_from_str_empty() {
1269 let yaml = "";
1271 let doc = Document::from_str(yaml).unwrap();
1272
1273 assert!(doc.is_empty());
1275 }
1276
1277 #[test]
1278 fn test_document_from_str_bare_document() {
1279 let yaml = "name: test\nversion: 1.0";
1281 let doc = Document::from_str(yaml).unwrap();
1282
1283 assert_eq!(doc.get_string("name"), Some("test".to_string()));
1284 assert_eq!(doc.get_string("version"), Some("1.0".to_string()));
1285 }
1286
1287 #[test]
1288 fn test_document_from_str_with_explicit_marker() {
1289 let yaml = "---\nkey: value";
1291 let doc = Document::from_str(yaml).unwrap();
1292
1293 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1294 }
1295
1296 #[test]
1297 fn test_document_from_str_complex_structure() {
1298 let yaml = r#"
1300database:
1301 host: localhost
1302 port: 5432
1303 credentials:
1304 username: admin
1305 password: secret
1306features:
1307 - auth
1308 - logging
1309 - metrics
1310"#;
1311 let doc = Document::from_str(yaml).unwrap();
1312
1313 assert!(doc.contains_key("database"));
1315 assert!(doc.contains_key("features"));
1316
1317 let db = doc.get("database").unwrap();
1319 if let Some(db_map) = db.as_mapping() {
1320 assert!(db_map.contains_key("host"));
1321 } else {
1322 panic!("Expected mapping for database");
1323 }
1324 }
1325
1326 #[test]
1327 fn test_is_sequence_block() {
1328 let doc = Document::from_str("tags:\n - alpha\n - beta\n - gamma").unwrap();
1329 assert!(doc.is_sequence("tags"));
1330 assert_eq!(
1331 doc.get_sequence("tags")
1332 .and_then(|s| s.get(0))
1333 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1334 Some("alpha".to_string())
1335 );
1336 }
1337
1338 #[test]
1339 fn test_is_sequence_flow() {
1340 let doc = Document::from_str("tags: [alpha, beta, gamma]").unwrap();
1341 assert!(doc.is_sequence("tags"));
1342 assert_eq!(
1343 doc.get_sequence("tags")
1344 .and_then(|s| s.get(0))
1345 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1346 Some("alpha".to_string())
1347 );
1348 }
1349
1350 #[test]
1351 fn test_sequence_first_element_flow_quoted_with_comma() {
1352 let doc = Document::from_str("tags: [\"hello, world\", beta]").unwrap();
1354 assert_eq!(
1355 doc.get_sequence("tags")
1356 .and_then(|s| s.get(0))
1357 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1358 Some("hello, world".to_string())
1359 );
1360 }
1361
1362 #[test]
1363 fn test_is_sequence_missing_key() {
1364 let doc = Document::from_str("name: test").unwrap();
1365 assert!(!doc.is_sequence("tags"));
1366 }
1367
1368 #[test]
1369 fn test_is_sequence_not_sequence() {
1370 let doc = Document::from_str("name: test").unwrap();
1371 assert!(!doc.is_sequence("name"));
1372 }
1373
1374 #[test]
1375 fn test_is_sequence_empty_sequence() {
1376 let doc = Document::from_str("tags: []").unwrap();
1377 assert!(doc.is_sequence("tags"));
1378 assert_eq!(doc.get_sequence("tags").map(|s| s.len()), Some(0));
1379 }
1380
1381 #[test]
1382 fn test_get_string_plain_scalar() {
1383 let doc = Document::from_str("key: hello").unwrap();
1384 assert_eq!(doc.get_string("key"), Some("hello".to_string()));
1385 }
1386
1387 #[test]
1388 fn test_get_string_double_quoted_with_escapes() {
1389 let doc = Document::from_str(r#"key: "hello \"world\"""#).unwrap();
1390 assert_eq!(doc.get_string("key"), Some(r#"hello "world""#.to_string()));
1391 }
1392
1393 #[test]
1394 fn test_get_string_single_quoted() {
1395 let doc = Document::from_str("key: 'it''s fine'").unwrap();
1396 assert_eq!(doc.get_string("key"), Some("it's fine".to_string()));
1397 }
1398
1399 #[test]
1400 fn test_get_string_missing_key() {
1401 let doc = Document::from_str("other: value").unwrap();
1402 assert_eq!(doc.get_string("key"), None);
1403 }
1404
1405 #[test]
1406 fn test_get_string_sequence_value_returns_none() {
1407 let doc = Document::from_str("key:\n - a\n - b").unwrap();
1408 assert_eq!(doc.get_string("key"), None);
1409 }
1410
1411 #[test]
1412 fn test_get_string_mapping_value_returns_none() {
1413 let doc = Document::from_str("key:\n nested: value").unwrap();
1414 assert_eq!(doc.get_string("key"), None);
1415 }
1416
1417 #[test]
1418 fn test_insert_after_preserves_newline() {
1419 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new\n";
1421 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1422
1423 if let Some(doc) = yaml_obj.document() {
1425 let result = doc.insert_after(
1426 "Bug-Submit",
1427 "Repository",
1428 "https://github.com/example/example.git",
1429 );
1430 assert!(result, "insert_after should return true when key is found");
1431
1432 let output = doc.to_string();
1434
1435 let expected = "---
1436Bug-Database: https://github.com/example/example/issues
1437Bug-Submit: https://github.com/example/example/issues/new
1438Repository: https://github.com/example/example.git
1439";
1440 assert_eq!(output, expected);
1441 }
1442 }
1443
1444 #[test]
1445 fn test_insert_after_without_trailing_newline() {
1446 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new";
1448 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1449
1450 if let Some(doc) = yaml_obj.document() {
1451 let result = doc.insert_after(
1452 "Bug-Submit",
1453 "Repository",
1454 "https://github.com/example/example.git",
1455 );
1456 assert!(result, "insert_after should return true when key is found");
1457
1458 let output = doc.to_string();
1459
1460 let expected = "---
1461Bug-Database: https://github.com/example/example/issues
1462Bug-Submit: https://github.com/example/example/issues/new
1463Repository: https://github.com/example/example.git
1464";
1465 assert_eq!(output, expected);
1466 }
1467 }
1468
1469 #[test]
1470 fn test_document_from_str_leading_comment_errors() {
1471 let yaml = "# leading comment\nkey: value\n";
1475 let err = Document::from_str(yaml).unwrap_err();
1476 match err {
1477 crate::error::YamlError::InvalidOperation { operation, reason } => {
1478 assert_eq!(operation, "Document::from_str");
1479 assert!(
1480 reason.contains("stream-level comments"),
1481 "unexpected reason: {reason}"
1482 );
1483 }
1484 other => panic!("expected InvalidOperation, got {other:?}"),
1485 }
1486 }
1487
1488 #[test]
1489 fn test_document_from_str_directive_errors() {
1490 let yaml = "%YAML 1.2\n---\nkey: value\n";
1493 let err = Document::from_str(yaml).unwrap_err();
1494 match err {
1495 crate::error::YamlError::InvalidOperation { operation, reason } => {
1496 assert_eq!(operation, "Document::from_str");
1497 assert!(reason.contains("directives"), "unexpected reason: {reason}");
1498 }
1499 other => panic!("expected InvalidOperation, got {other:?}"),
1500 }
1501 }
1502
1503 #[test]
1504 fn test_document_from_str_trailing_comment_errors() {
1505 let yaml = "key: value\n...\n# trailing\n";
1507 assert!(matches!(
1508 Document::from_str(yaml),
1509 Err(crate::error::YamlError::InvalidOperation { .. })
1510 ));
1511 }
1512
1513 #[test]
1514 fn test_document_from_str_ignoring_comments_drops_prefix() {
1515 let yaml = "# leading\n%YAML 1.2\n---\nkey: value\n";
1516 let doc = Document::from_str_ignoring_comments(yaml).unwrap();
1517 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1518 assert_eq!(doc.to_string(), "---\nkey: value\n");
1521 }
1522
1523 #[test]
1524 fn test_document_from_str_ignoring_comments_rejects_multi_document() {
1525 let yaml = "---\na: 1\n---\nb: 2\n";
1526 assert!(matches!(
1527 Document::from_str_ignoring_comments(yaml),
1528 Err(crate::error::YamlError::InvalidOperation { .. })
1529 ));
1530 }
1531
1532 #[test]
1533 fn test_document_from_str_interior_comment_ok() {
1534 let yaml = "key: value # trailing\n";
1537 let doc = Document::from_str(yaml).unwrap();
1538 assert_eq!(doc.to_string(), yaml);
1539 }
1540
1541 #[test]
1542 fn test_from_str_relaxed_valid() {
1543 let (doc, errors) = Document::from_str_relaxed("key: value\n");
1544 assert!(errors.is_empty());
1545 let mapping = doc.as_mapping().unwrap();
1546 assert_eq!(
1547 mapping.get("key").unwrap().as_scalar().unwrap().to_string(),
1548 "value"
1549 );
1550 }
1551
1552 #[test]
1553 fn test_from_str_relaxed_with_errors() {
1554 let (doc, errors) = Document::from_str_relaxed("key: [unclosed");
1555 assert!(!errors.is_empty());
1556 assert!(doc.as_mapping().is_some());
1558 }
1559
1560 #[test]
1561 fn test_from_str_relaxed_multi_document() {
1562 let (doc, errors) = Document::from_str_relaxed("a: 1\n---\nb: 2\n");
1564 assert!(errors.is_empty());
1565 let mapping = doc.as_mapping().unwrap();
1566 assert_eq!(
1567 mapping.get("a").unwrap().as_scalar().unwrap().to_string(),
1568 "1"
1569 );
1570 }
1571
1572 #[test]
1573 fn test_from_str_relaxed_empty_input() {
1574 let (doc, errors) = Document::from_str_relaxed("");
1575 assert!(errors.is_empty());
1576 assert!(doc.as_mapping().is_none());
1578 }
1579}