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 to_file<P: AsRef<Path>>(&self, path: P) -> YamlResult<()> {
53 let path = path.as_ref();
54 if let Some(parent) = path.parent() {
55 std::fs::create_dir_all(parent)?;
56 }
57 let mut content = self.to_string();
58 if !content.ends_with('\n') {
60 content.push('\n');
61 }
62 std::fs::write(path, content)?;
63 Ok(())
64 }
65
66 pub(crate) fn root_node(&self) -> Option<SyntaxNode> {
68 self.0.children().find(|child| {
69 matches!(
70 child.kind(),
71 SyntaxKind::MAPPING
72 | SyntaxKind::SEQUENCE
73 | SyntaxKind::SCALAR
74 | SyntaxKind::ALIAS
75 | SyntaxKind::TAGGED_NODE
76 )
77 })
78 }
79
80 pub fn as_mapping(&self) -> Option<Mapping> {
86 self.root_node().and_then(Mapping::cast)
87 }
88
89 pub fn as_sequence(&self) -> Option<Sequence> {
91 self.root_node().and_then(Sequence::cast)
92 }
93
94 pub fn as_scalar(&self) -> Option<Scalar> {
96 self.root_node().and_then(Scalar::cast)
97 }
98
99 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
101 self.as_mapping().is_some_and(|m| m.contains_key(key))
102 }
103
104 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
108 self.as_mapping().and_then(|m| m.get(key))
109 }
110
111 pub(crate) fn get_node(&self, key: impl crate::AsYaml) -> Option<SyntaxNode> {
116 self.as_mapping().and_then(|m| m.get_node(key))
117 }
118
119 pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) {
121 if let Some(mapping) = self.as_mapping() {
122 mapping.set(key, value);
123 } else {
125 let mapping = Mapping::new();
127 mapping.set(key, value);
128
129 let child_count = self.0.children_with_tokens().count();
131 self.0
132 .splice_children(child_count..child_count, vec![mapping.0.into()]);
133 }
134 }
135
136 pub fn set_with_field_order<I, K>(
142 &self,
143 key: impl crate::AsYaml,
144 value: impl crate::AsYaml,
145 field_order: I,
146 ) where
147 I: IntoIterator<Item = K>,
148 K: crate::AsYaml,
149 {
150 let field_order: Vec<K> = field_order.into_iter().collect();
152 if let Some(mapping) = self.as_mapping() {
153 mapping.set_with_field_order(key, value, field_order);
154 } else {
156 let mapping = Mapping::new();
158 mapping.set_with_field_order(key, value, field_order);
159 let child_count = self.0.children_with_tokens().count();
160 self.0
161 .splice_children(child_count..child_count, vec![mapping.0.into()]);
162 }
163 }
164
165 pub fn remove(&self, key: impl crate::AsYaml) -> Option<super::MappingEntry> {
172 self.as_mapping()?.remove(key)
173 }
174
175 pub(crate) fn key_nodes(&self) -> impl Iterator<Item = SyntaxNode> + '_ {
177 self.as_mapping()
178 .into_iter()
179 .flat_map(|m| m.key_nodes().collect::<Vec<_>>())
180 }
181
182 pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
188 self.key_nodes().filter_map(|key_node| {
189 key_node
190 .children()
191 .next()
192 .and_then(crate::as_yaml::YamlNode::from_syntax)
193 })
194 }
195
196 pub fn is_empty(&self) -> bool {
198 self.as_mapping().map_or(true, |m| m.is_empty())
199 }
200
201 pub fn from_mapping(mapping: Mapping) -> Self {
203 let mut builder = GreenNodeBuilder::new();
205 builder.start_node(SyntaxKind::DOCUMENT.into());
206 builder.token(SyntaxKind::DOC_START.into(), "---");
208 builder.token(SyntaxKind::WHITESPACE.into(), "\n");
209
210 builder.start_node(SyntaxKind::MAPPING.into());
212 let mapping_green = mapping.0.green();
213 let children: Vec<_> = mapping_green.children().collect();
214
215 let end_index = if let Some(rowan::NodeOrToken::Token(t)) = children.last() {
217 if t.kind() == SyntaxKind::NEWLINE.into() {
218 children.len() - 1
219 } else {
220 children.len()
221 }
222 } else {
223 children.len()
224 };
225
226 for child in &children[..end_index] {
227 match child {
228 rowan::NodeOrToken::Node(n) => {
229 builder.start_node(n.kind());
230 Self::add_green_node_children(&mut builder, n);
231 builder.finish_node();
232 }
233 rowan::NodeOrToken::Token(t) => {
234 builder.token(t.kind(), t.text());
235 }
236 }
237 }
238
239 builder.finish_node(); builder.finish_node(); Document(SyntaxNode::new_root_mut(builder.finish()))
244 }
245
246 fn add_green_node_children(builder: &mut GreenNodeBuilder, node: &rowan::GreenNodeData) {
248 for child in node.children() {
249 match child {
250 rowan::NodeOrToken::Node(n) => {
251 builder.start_node(n.kind());
252 Self::add_green_node_children(builder, n);
253 builder.finish_node();
254 }
255 rowan::NodeOrToken::Token(t) => {
256 builder.token(t.kind(), t.text());
257 }
258 }
259 }
260 }
261
262 pub fn insert_after(
271 &self,
272 after_key: impl crate::AsYaml,
273 key: impl crate::AsYaml,
274 value: impl crate::AsYaml,
275 ) -> bool {
276 if let Some(mapping) = self.as_mapping() {
277 mapping.insert_after(after_key, key, value)
278 } else {
279 false
280 }
281 }
282
283 pub fn move_after(
292 &self,
293 after_key: impl crate::AsYaml,
294 key: impl crate::AsYaml,
295 value: impl crate::AsYaml,
296 ) -> bool {
297 if let Some(mapping) = self.as_mapping() {
298 mapping.move_after(after_key, key, value)
299 } else {
300 false
301 }
302 }
303
304 pub fn insert_before(
313 &self,
314 before_key: impl crate::AsYaml,
315 key: impl crate::AsYaml,
316 value: impl crate::AsYaml,
317 ) -> bool {
318 if let Some(mapping) = self.as_mapping() {
319 mapping.insert_before(before_key, key, value)
320 } else {
321 false
322 }
323 }
324
325 pub fn move_before(
334 &self,
335 before_key: impl crate::AsYaml,
336 key: impl crate::AsYaml,
337 value: impl crate::AsYaml,
338 ) -> bool {
339 if let Some(mapping) = self.as_mapping() {
340 mapping.move_before(before_key, key, value)
341 } else {
342 false
343 }
344 }
345
346 pub(crate) fn build_value_content(
348 builder: &mut GreenNodeBuilder,
349 value: impl crate::AsYaml,
350 indent: usize,
351 ) {
352 builder.start_node(SyntaxKind::VALUE.into());
353 value.build_content(builder, indent, false);
354 builder.finish_node(); }
356
357 pub fn insert_at_index(
363 &self,
364 index: usize,
365 key: impl crate::AsYaml,
366 value: impl crate::AsYaml,
367 ) {
368 if let Some(mapping) = self.as_mapping() {
370 mapping.insert_at_index(index, key, value);
371 return;
372 }
373
374 let mapping = Mapping::new();
376 mapping.insert_at_index_preserving(index, key, value);
377 let new_doc = Self::from_mapping(mapping);
378 let new_children: Vec<_> = new_doc.0.children_with_tokens().collect();
379 let child_count = self.0.children_with_tokens().count();
380 self.0.splice_children(0..child_count, new_children);
381 }
382
383 pub fn get_string(&self, key: impl crate::AsYaml) -> Option<String> {
391 let content = self.get_node(key)?;
394 if let Some(tagged_node) = TaggedNode::cast(content.clone()) {
395 tagged_node.value().map(|s| s.as_string())
396 } else {
397 Scalar::cast(content).map(|s| s.as_string())
399 }
400 }
401
402 pub fn len(&self) -> usize {
406 self.as_mapping().map(|m| m.len()).unwrap_or(0)
407 }
408
409 pub fn get_mapping(&self, key: impl crate::AsYaml) -> Option<Mapping> {
413 self.get(key).and_then(|n| n.as_mapping().cloned())
414 }
415
416 pub fn get_sequence(&self, key: impl crate::AsYaml) -> Option<Sequence> {
420 self.get(key).and_then(|n| n.as_sequence().cloned())
421 }
422
423 pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
428 self.as_mapping()
429 .map(|m| m.rename_key(old_key, new_key))
430 .unwrap_or(false)
431 }
432
433 pub fn is_sequence(&self, key: impl crate::AsYaml) -> bool {
435 self.get(key)
436 .map(|node| node.as_sequence().is_some())
437 .unwrap_or(false)
438 }
439
440 pub fn reorder_fields<I, K>(&self, order: I)
445 where
446 I: IntoIterator<Item = K>,
447 K: crate::AsYaml,
448 {
449 if let Some(mapping) = self.as_mapping() {
450 mapping.reorder_fields(order);
451 }
452 }
453
454 pub fn validate_schema(
482 &self,
483 validator: &crate::schema::SchemaValidator,
484 ) -> crate::schema::ValidationResult<()> {
485 validator.validate(self)
486 }
487
488 pub fn byte_range(&self) -> crate::TextPosition {
504 self.0.text_range().into()
505 }
506
507 pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
529 let range = self.byte_range();
530 crate::byte_offset_to_line_column(source_text, range.start as usize)
531 }
532
533 pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
542 let range = self.byte_range();
543 crate::byte_offset_to_line_column(source_text, range.end as usize)
544 }
545}
546
547impl Default for Document {
548 fn default() -> Self {
549 Self::new()
550 }
551}
552
553impl std::str::FromStr for Document {
554 type Err = crate::error::YamlError;
555
556 fn from_str(s: &str) -> Result<Self, Self::Err> {
570 let parsed = YamlFile::parse(s);
571
572 if !parsed.positioned_errors().is_empty() {
573 let first_error = &parsed.positioned_errors()[0];
574 let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
575 return Err(crate::error::YamlError::Parse {
576 message: first_error.message.clone(),
577 line: Some(lc.line),
578 column: Some(lc.column),
579 });
580 }
581
582 let mut docs = parsed.tree().documents();
583 let first = docs.next().unwrap_or_default();
584
585 if docs.next().is_some() {
586 return Err(crate::error::YamlError::InvalidOperation {
587 operation: "Document::from_str".to_string(),
588 reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
589 });
590 }
591
592 Ok(first)
593 }
594}
595
596impl AsYaml for Document {
597 fn as_node(&self) -> Option<&SyntaxNode> {
598 Some(&self.0)
599 }
600
601 fn kind(&self) -> YamlKind {
602 YamlKind::Document
603 }
604
605 fn build_content(
606 &self,
607 builder: &mut rowan::GreenNodeBuilder,
608 _indent: usize,
609 _flow_context: bool,
610 ) -> bool {
611 crate::as_yaml::copy_node_content(builder, &self.0);
612 self.0
613 .last_token()
614 .map(|t| t.kind() == SyntaxKind::NEWLINE)
615 .unwrap_or(false)
616 }
617
618 fn is_inline(&self) -> bool {
619 false
621 }
622}
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::builder::{MappingBuilder, SequenceBuilder};
627 use crate::yaml::YamlFile;
628 use std::str::FromStr;
629
630 #[test]
631 fn test_document_stream_features() {
632 let yaml1 = "---\ndoc1: first\n---\ndoc2: second\n...\n";
634 let parsed1 = YamlFile::from_str(yaml1).unwrap();
635 assert_eq!(parsed1.documents().count(), 2);
636 assert_eq!(parsed1.to_string(), yaml1);
637
638 let yaml2 = "---\nkey: value\n...\n";
640 let parsed2 = YamlFile::from_str(yaml2).unwrap();
641 assert_eq!(parsed2.documents().count(), 1);
642 assert_eq!(parsed2.to_string(), yaml2);
643
644 let yaml3 = "key: value\n...\n";
646 let parsed3 = YamlFile::from_str(yaml3).unwrap();
647 assert_eq!(parsed3.documents().count(), 1);
648 assert_eq!(parsed3.to_string(), yaml3);
649 }
650 #[test]
651 fn test_document_level_directives() {
652 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";
654 let parsed = YamlFile::from_str(yaml).unwrap();
655 assert_eq!(parsed.documents().count(), 2);
656 assert_eq!(parsed.to_string(), yaml);
657 }
658 #[test]
659 fn test_document_schema_validation_api() {
660 let json_yaml = r#"
664name: "John"
665age: 30
666active: true
667items:
668 - "apple"
669 - 42
670 - true
671"#;
672 let doc = YamlFile::from_str(json_yaml).unwrap().document().unwrap();
673
674 assert!(
676 crate::schema::SchemaValidator::json()
677 .validate(&doc)
678 .is_ok(),
679 "JSON-compatible document should pass JSON validation"
680 );
681
682 assert!(
684 crate::schema::SchemaValidator::core()
685 .validate(&doc)
686 .is_ok(),
687 "Valid document should pass Core validation"
688 );
689
690 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
693 assert!(
694 doc.validate_schema(&failsafe_strict).is_err(),
695 "Document with numbers and booleans should fail strict Failsafe validation"
696 );
697
698 let yaml_specific = r#"
700name: "Test"
701created: 2023-12-25T10:30:45Z
702pattern: !!regex '\d+'
703data: !!binary "SGVsbG8="
704"#;
705 let yaml_doc = YamlFile::from_str(yaml_specific)
706 .unwrap()
707 .document()
708 .unwrap();
709
710 assert!(
712 crate::schema::SchemaValidator::core()
713 .validate(&yaml_doc)
714 .is_ok(),
715 "YAML-specific types should pass Core validation"
716 );
717
718 assert!(
720 crate::schema::SchemaValidator::json()
721 .validate(&yaml_doc)
722 .is_err(),
723 "YAML-specific types should fail JSON validation"
724 );
725
726 assert!(
728 crate::schema::SchemaValidator::failsafe()
729 .validate(&yaml_doc)
730 .is_err(),
731 "YAML-specific types should fail Failsafe validation"
732 );
733
734 let string_only = r#"
736name: hello
737message: world
738items:
739 - apple
740 - banana
741nested:
742 key: value
743"#;
744 let str_doc = YamlFile::from_str(string_only).unwrap().document().unwrap();
745
746 assert!(
748 crate::schema::SchemaValidator::failsafe()
749 .validate(&str_doc)
750 .is_ok(),
751 "String-only document should pass Failsafe validation"
752 );
753 assert!(
754 crate::schema::SchemaValidator::json()
755 .validate(&str_doc)
756 .is_ok(),
757 "String-only document should pass JSON validation"
758 );
759 assert!(
760 crate::schema::SchemaValidator::core()
761 .validate(&str_doc)
762 .is_ok(),
763 "String-only document should pass Core validation"
764 );
765 }
766 #[test]
767 fn test_document_set_preserves_position() {
768 let yaml = r#"Name: original
770Version: 1.0
771Author: Someone
772"#;
773 let parsed = YamlFile::from_str(yaml).unwrap();
774 let doc = parsed.document().expect("Should have a document");
775
776 doc.set("Version", 2.0);
778
779 let output = doc.to_string();
780 let expected = r#"Name: original
781Version: 2.0
782Author: Someone
783"#;
784 assert_eq!(output, expected);
785 }
786 #[test]
787 fn test_document_schema_coercion_api() {
788 let coercion_yaml = r#"
790count: "42"
791enabled: "true"
792rate: "3.14"
793items:
794 - "100"
795 - "false"
796"#;
797 let doc = YamlFile::from_str(coercion_yaml)
798 .unwrap()
799 .document()
800 .unwrap();
801 let json_validator = crate::schema::SchemaValidator::json();
802
803 assert!(
805 json_validator.can_coerce(&doc).is_ok(),
806 "Strings that look like numbers/booleans should be coercible to JSON types"
807 );
808
809 let non_coercible = r#"
811timestamp: !!timestamp "2023-01-01"
812pattern: !!regex '\d+'
813"#;
814 let non_coer_doc = YamlFile::from_str(non_coercible)
815 .unwrap()
816 .document()
817 .unwrap();
818
819 assert!(
821 json_validator.can_coerce(&non_coer_doc).is_err(),
822 "YAML-specific types should not be coercible to JSON schema"
823 );
824 }
825 #[test]
826 fn test_document_schema_validation_errors() {
827 let nested_yaml = r#"
829users:
830 - name: "Alice"
831 age: 25
832 metadata:
833 created: !!timestamp "2023-01-01"
834 active: true
835 - name: "Bob"
836 score: 95.5
837"#;
838 let doc = YamlFile::from_str(nested_yaml).unwrap().document().unwrap();
839
840 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
842 let failsafe_result = doc.validate_schema(&failsafe_strict);
843 assert!(
844 failsafe_result.is_err(),
845 "Nested document with numbers should fail strict Failsafe validation"
846 );
847
848 let errors = failsafe_result.unwrap_err();
849 assert!(!errors.is_empty());
850
851 for error in &errors {
853 assert!(!error.path.is_empty(), "Error should have path: {}", error);
854 }
855
856 let json_result = crate::schema::SchemaValidator::json().validate(&doc);
858 assert!(
859 json_result.is_err(),
860 "Document with timestamp should fail JSON validation"
861 );
862
863 let json_errors = json_result.unwrap_err();
864 assert!(!json_errors.is_empty());
865 assert!(
867 json_errors.iter().any(|e| e.schema_name == "json"),
868 "Should have JSON schema validation error"
869 );
870 }
871 #[test]
872 fn test_document_schema_validation_with_custom_validator() {
873 let yaml = r#"
875name: "HelloWorld"
876count: 42
877active: true
878"#;
879 let doc = YamlFile::from_str(yaml).unwrap().document().unwrap();
880
881 let json_validator = crate::schema::SchemaValidator::json();
883 let core_validator = crate::schema::SchemaValidator::core();
884
885 assert!(
887 doc.validate_schema(&core_validator).is_ok(),
888 "Should pass Core validation"
889 );
890 assert!(
891 doc.validate_schema(&json_validator).is_ok(),
892 "Should pass JSON validation"
893 );
894 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
896 assert!(
897 doc.validate_schema(&failsafe_strict).is_err(),
898 "Should fail strict Failsafe validation"
899 );
900
901 let strict_json = crate::schema::SchemaValidator::json().strict();
903 assert!(
905 doc.validate_schema(&strict_json).is_ok(),
906 "Should pass strict JSON validation (integers and booleans are JSON-compatible)"
907 );
908
909 let strict_failsafe = crate::schema::SchemaValidator::failsafe().strict();
910 assert!(
911 doc.validate_schema(&strict_failsafe).is_err(),
912 "Should fail strict Failsafe validation"
913 );
914 }
915 #[test]
916 fn test_document_level_insertion_with_complex_types() {
917 let doc1 = Document::new();
921 doc1.set("name", "project");
922 let features = SequenceBuilder::new()
923 .item("auth")
924 .item("api")
925 .item("web")
926 .build_document()
927 .as_sequence()
928 .unwrap();
929 let success = doc1.insert_after("name", "features", features);
930 assert!(success);
931 let output1 = doc1.to_string();
932 assert_eq!(
933 output1,
934 "---\nname: project\nfeatures:\n - auth\n - api\n - web\n"
935 );
936
937 let doc2 = Document::new();
939 doc2.set("name", "project");
940 doc2.set("version", "1.0.0");
941 let database = MappingBuilder::new()
942 .pair("host", "localhost")
943 .pair("port", 5432)
944 .build_document()
945 .as_mapping()
946 .unwrap();
947 let success = doc2.insert_before("version", "database", database);
948 assert!(success);
949 let output2 = doc2.to_string();
950 assert_eq!(
951 output2,
952 "---\nname: project\ndatabase:\n host: localhost\n port: 5432\nversion: 1.0.0\n"
953 );
954
955 let doc3 = Document::new();
957 doc3.set("name", "project");
958 #[allow(clippy::disallowed_types)]
960 let tag_set = {
961 use crate::value::YamlValue;
962 let mut tags = std::collections::BTreeSet::new();
963 tags.insert("production".to_string());
964 tags.insert("database".to_string());
965 YamlValue::from_set(tags)
966 };
967 doc3.insert_at_index(1, "tags", tag_set);
968 let output3 = doc3.to_string();
969 assert_eq!(
970 output3,
971 "---\nname: project\ntags: !!set\n database: null\n production: null\n"
972 );
973
974 assert!(
976 YamlFile::from_str(&output1).is_ok(),
977 "Sequence output should be valid YAML"
978 );
979 assert!(
980 YamlFile::from_str(&output2).is_ok(),
981 "Mapping output should be valid YAML"
982 );
983 assert!(
984 YamlFile::from_str(&output3).is_ok(),
985 "Set output should be valid YAML"
986 );
987 }
988
989 #[test]
990 fn test_document_api_usage() -> crate::error::YamlResult<()> {
991 let doc = Document::new();
993
994 assert!(!doc.contains_key("Repository"));
996 doc.set("Repository", "https://github.com/user/repo.git");
997 assert!(doc.contains_key("Repository"));
998
999 assert_eq!(
1001 doc.get_string("Repository"),
1002 Some("https://github.com/user/repo.git".to_string())
1003 );
1004
1005 assert!(!doc.is_empty());
1007
1008 let keys: Vec<_> = doc.keys().collect();
1010 assert_eq!(keys.len(), 1);
1011
1012 assert!(doc.remove("Repository").is_some());
1014 assert!(!doc.contains_key("Repository"));
1015 assert!(doc.is_empty());
1016
1017 Ok(())
1018 }
1019
1020 #[test]
1021 fn test_field_ordering() {
1022 let doc = Document::new();
1023
1024 doc.set("Repository-Browse", "https://github.com/user/repo");
1026 doc.set("Name", "MyProject");
1027 doc.set("Bug-Database", "https://github.com/user/repo/issues");
1028 doc.set("Repository", "https://github.com/user/repo.git");
1029
1030 doc.reorder_fields(["Name", "Bug-Database", "Repository", "Repository-Browse"]);
1032
1033 let keys: Vec<_> = doc.keys().collect();
1035 assert_eq!(keys.len(), 4);
1036 assert_eq!(
1037 keys[0].as_scalar().map(|s| s.as_string()),
1038 Some("Name".to_string())
1039 );
1040 assert_eq!(
1041 keys[1].as_scalar().map(|s| s.as_string()),
1042 Some("Bug-Database".to_string())
1043 );
1044 assert_eq!(
1045 keys[2].as_scalar().map(|s| s.as_string()),
1046 Some("Repository".to_string())
1047 );
1048 assert_eq!(
1049 keys[3].as_scalar().map(|s| s.as_string()),
1050 Some("Repository-Browse".to_string())
1051 );
1052 }
1053
1054 #[test]
1055 fn test_array_detection() {
1056 use crate::scalar::ScalarValue;
1057
1058 let doc = Document::new();
1060
1061 let array_value = SequenceBuilder::new()
1063 .item(ScalarValue::string("https://github.com/user/repo.git"))
1064 .item(ScalarValue::string("https://gitlab.com/user/repo.git"))
1065 .build_document()
1066 .as_sequence()
1067 .unwrap();
1068 doc.set("Repository", &array_value);
1069
1070 assert!(doc.is_sequence("Repository"));
1072 assert!(doc.get_sequence("Repository").is_some());
1075 }
1076
1077 #[test]
1078 fn test_file_io() -> crate::error::YamlResult<()> {
1079 use std::fs;
1080
1081 let test_path = "/tmp/test_yaml_edit.yaml";
1083
1084 let doc = Document::new();
1086 doc.set("Name", "TestProject");
1087 doc.set("Repository", "https://example.com/repo.git");
1088
1089 doc.to_file(test_path)?;
1090
1091 let loaded_doc = Document::from_file(test_path)?;
1093
1094 assert_eq!(
1095 loaded_doc.get_string("Name"),
1096 Some("TestProject".to_string())
1097 );
1098 assert_eq!(
1099 loaded_doc.get_string("Repository"),
1100 Some("https://example.com/repo.git".to_string())
1101 );
1102
1103 let _ = fs::remove_file(test_path);
1105
1106 Ok(())
1107 }
1108
1109 #[test]
1110 fn test_document_from_str_single_document() {
1111 let yaml = "key: value\nport: 8080";
1113 let doc = Document::from_str(yaml).unwrap();
1114
1115 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1116 assert!(doc.contains_key("port"));
1117 }
1118
1119 #[test]
1120 fn test_document_from_str_multiple_documents_error() {
1121 let yaml = "---\nkey: value\n---\nother: data";
1123 let result = Document::from_str(yaml);
1124
1125 assert!(result.is_err());
1126 let err = result.unwrap_err();
1127 match err {
1128 crate::error::YamlError::InvalidOperation { operation, reason } => {
1129 assert_eq!(operation, "Document::from_str");
1130 assert_eq!(
1131 reason,
1132 "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML."
1133 );
1134 }
1135 _ => panic!("Expected InvalidOperation error, got {:?}", err),
1136 }
1137 }
1138
1139 #[test]
1140 fn test_document_from_str_empty() {
1141 let yaml = "";
1143 let doc = Document::from_str(yaml).unwrap();
1144
1145 assert!(doc.is_empty());
1147 }
1148
1149 #[test]
1150 fn test_document_from_str_bare_document() {
1151 let yaml = "name: test\nversion: 1.0";
1153 let doc = Document::from_str(yaml).unwrap();
1154
1155 assert_eq!(doc.get_string("name"), Some("test".to_string()));
1156 assert_eq!(doc.get_string("version"), Some("1.0".to_string()));
1157 }
1158
1159 #[test]
1160 fn test_document_from_str_with_explicit_marker() {
1161 let yaml = "---\nkey: value";
1163 let doc = Document::from_str(yaml).unwrap();
1164
1165 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1166 }
1167
1168 #[test]
1169 fn test_document_from_str_complex_structure() {
1170 let yaml = r#"
1172database:
1173 host: localhost
1174 port: 5432
1175 credentials:
1176 username: admin
1177 password: secret
1178features:
1179 - auth
1180 - logging
1181 - metrics
1182"#;
1183 let doc = Document::from_str(yaml).unwrap();
1184
1185 assert!(doc.contains_key("database"));
1187 assert!(doc.contains_key("features"));
1188
1189 let db = doc.get("database").unwrap();
1191 if let Some(db_map) = db.as_mapping() {
1192 assert!(db_map.contains_key("host"));
1193 } else {
1194 panic!("Expected mapping for database");
1195 }
1196 }
1197
1198 #[test]
1199 fn test_is_sequence_block() {
1200 let doc = Document::from_str("tags:\n - alpha\n - beta\n - gamma").unwrap();
1201 assert!(doc.is_sequence("tags"));
1202 assert_eq!(
1203 doc.get_sequence("tags")
1204 .and_then(|s| s.get(0))
1205 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1206 Some("alpha".to_string())
1207 );
1208 }
1209
1210 #[test]
1211 fn test_is_sequence_flow() {
1212 let doc = Document::from_str("tags: [alpha, beta, gamma]").unwrap();
1213 assert!(doc.is_sequence("tags"));
1214 assert_eq!(
1215 doc.get_sequence("tags")
1216 .and_then(|s| s.get(0))
1217 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1218 Some("alpha".to_string())
1219 );
1220 }
1221
1222 #[test]
1223 fn test_sequence_first_element_flow_quoted_with_comma() {
1224 let doc = Document::from_str("tags: [\"hello, world\", beta]").unwrap();
1226 assert_eq!(
1227 doc.get_sequence("tags")
1228 .and_then(|s| s.get(0))
1229 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1230 Some("hello, world".to_string())
1231 );
1232 }
1233
1234 #[test]
1235 fn test_is_sequence_missing_key() {
1236 let doc = Document::from_str("name: test").unwrap();
1237 assert!(!doc.is_sequence("tags"));
1238 }
1239
1240 #[test]
1241 fn test_is_sequence_not_sequence() {
1242 let doc = Document::from_str("name: test").unwrap();
1243 assert!(!doc.is_sequence("name"));
1244 }
1245
1246 #[test]
1247 fn test_is_sequence_empty_sequence() {
1248 let doc = Document::from_str("tags: []").unwrap();
1249 assert!(doc.is_sequence("tags"));
1250 assert_eq!(doc.get_sequence("tags").map(|s| s.len()), Some(0));
1251 }
1252
1253 #[test]
1254 fn test_get_string_plain_scalar() {
1255 let doc = Document::from_str("key: hello").unwrap();
1256 assert_eq!(doc.get_string("key"), Some("hello".to_string()));
1257 }
1258
1259 #[test]
1260 fn test_get_string_double_quoted_with_escapes() {
1261 let doc = Document::from_str(r#"key: "hello \"world\"""#).unwrap();
1262 assert_eq!(doc.get_string("key"), Some(r#"hello "world""#.to_string()));
1263 }
1264
1265 #[test]
1266 fn test_get_string_single_quoted() {
1267 let doc = Document::from_str("key: 'it''s fine'").unwrap();
1268 assert_eq!(doc.get_string("key"), Some("it's fine".to_string()));
1269 }
1270
1271 #[test]
1272 fn test_get_string_missing_key() {
1273 let doc = Document::from_str("other: value").unwrap();
1274 assert_eq!(doc.get_string("key"), None);
1275 }
1276
1277 #[test]
1278 fn test_get_string_sequence_value_returns_none() {
1279 let doc = Document::from_str("key:\n - a\n - b").unwrap();
1280 assert_eq!(doc.get_string("key"), None);
1281 }
1282
1283 #[test]
1284 fn test_get_string_mapping_value_returns_none() {
1285 let doc = Document::from_str("key:\n nested: value").unwrap();
1286 assert_eq!(doc.get_string("key"), None);
1287 }
1288
1289 #[test]
1290 fn test_insert_after_preserves_newline() {
1291 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new\n";
1293 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1294
1295 if let Some(doc) = yaml_obj.document() {
1297 let result = doc.insert_after(
1298 "Bug-Submit",
1299 "Repository",
1300 "https://github.com/example/example.git",
1301 );
1302 assert!(result, "insert_after should return true when key is found");
1303
1304 let output = doc.to_string();
1306
1307 let expected = "---
1308Bug-Database: https://github.com/example/example/issues
1309Bug-Submit: https://github.com/example/example/issues/new
1310Repository: https://github.com/example/example.git
1311";
1312 assert_eq!(output, expected);
1313 }
1314 }
1315
1316 #[test]
1317 fn test_insert_after_without_trailing_newline() {
1318 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new";
1320 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1321
1322 if let Some(doc) = yaml_obj.document() {
1323 let result = doc.insert_after(
1324 "Bug-Submit",
1325 "Repository",
1326 "https://github.com/example/example.git",
1327 );
1328 assert!(result, "insert_after should return true when key is found");
1329
1330 let output = doc.to_string();
1331
1332 let expected = "---
1333Bug-Database: https://github.com/example/example/issues
1334Bug-Submit: https://github.com/example/example/issues/new
1335Repository: https://github.com/example/example.git
1336";
1337 assert_eq!(output, expected);
1338 }
1339 }
1340}