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_file_relaxed<P: AsRef<Path>>(
78 path: P,
79 ) -> Result<(Document, Vec<String>), std::io::Error> {
80 let content = std::fs::read_to_string(path)?;
81 Ok(Self::from_str_relaxed(&content))
82 }
83
84 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> YamlResult<()> {
89 let path = path.as_ref();
90 if let Some(parent) = path.parent() {
91 std::fs::create_dir_all(parent)?;
92 }
93 let mut content = self.to_string();
94 if !content.ends_with('\n') {
96 content.push('\n');
97 }
98 std::fs::write(path, content)?;
99 Ok(())
100 }
101
102 pub(crate) fn root_node(&self) -> Option<SyntaxNode> {
104 self.0.children().find(|child| {
105 matches!(
106 child.kind(),
107 SyntaxKind::MAPPING
108 | SyntaxKind::SEQUENCE
109 | SyntaxKind::SCALAR
110 | SyntaxKind::ALIAS
111 | SyntaxKind::TAGGED_NODE
112 )
113 })
114 }
115
116 pub fn as_mapping(&self) -> Option<Mapping> {
122 self.root_node().and_then(Mapping::cast)
123 }
124
125 pub fn as_sequence(&self) -> Option<Sequence> {
127 self.root_node().and_then(Sequence::cast)
128 }
129
130 pub fn as_scalar(&self) -> Option<Scalar> {
132 self.root_node().and_then(Scalar::cast)
133 }
134
135 pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
137 self.as_mapping().is_some_and(|m| m.contains_key(key))
138 }
139
140 pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
144 self.as_mapping().and_then(|m| m.get(key))
145 }
146
147 pub(crate) fn get_node(&self, key: impl crate::AsYaml) -> Option<SyntaxNode> {
152 self.as_mapping().and_then(|m| m.get_node(key))
153 }
154
155 pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) {
157 if let Some(mapping) = self.as_mapping() {
158 mapping.set(key, value);
159 } else {
161 let mapping = Mapping::new();
163 mapping.set(key, value);
164
165 let child_count = self.0.children_with_tokens().count();
167 self.0
168 .splice_children(child_count..child_count, vec![mapping.0.into()]);
169 }
170 }
171
172 pub fn set_with_field_order<I, K>(
178 &self,
179 key: impl crate::AsYaml,
180 value: impl crate::AsYaml,
181 field_order: I,
182 ) where
183 I: IntoIterator<Item = K>,
184 K: crate::AsYaml,
185 {
186 let field_order: Vec<K> = field_order.into_iter().collect();
188 if let Some(mapping) = self.as_mapping() {
189 mapping.set_with_field_order(key, value, field_order);
190 } else {
192 let mapping = Mapping::new();
194 mapping.set_with_field_order(key, value, field_order);
195 let child_count = self.0.children_with_tokens().count();
196 self.0
197 .splice_children(child_count..child_count, vec![mapping.0.into()]);
198 }
199 }
200
201 pub fn remove(&self, key: impl crate::AsYaml) -> Option<super::MappingEntry> {
208 self.as_mapping()?.remove(key)
209 }
210
211 pub(crate) fn key_nodes(&self) -> impl Iterator<Item = SyntaxNode> + '_ {
213 self.as_mapping()
214 .into_iter()
215 .flat_map(|m| m.key_nodes().collect::<Vec<_>>())
216 }
217
218 pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
224 self.key_nodes().filter_map(|key_node| {
225 key_node
226 .children()
227 .next()
228 .and_then(crate::as_yaml::YamlNode::from_syntax)
229 })
230 }
231
232 pub fn is_empty(&self) -> bool {
234 self.as_mapping().map_or(true, |m| m.is_empty())
235 }
236
237 pub fn from_mapping(mapping: Mapping) -> Self {
239 let mut builder = GreenNodeBuilder::new();
241 builder.start_node(SyntaxKind::DOCUMENT.into());
242 builder.token(SyntaxKind::DOC_START.into(), "---");
244 builder.token(SyntaxKind::WHITESPACE.into(), "\n");
245
246 builder.start_node(SyntaxKind::MAPPING.into());
248 let mapping_green = mapping.0.green();
249 let children: Vec<_> = mapping_green.children().collect();
250
251 let end_index = if let Some(rowan::NodeOrToken::Token(t)) = children.last() {
253 if t.kind() == SyntaxKind::NEWLINE.into() {
254 children.len() - 1
255 } else {
256 children.len()
257 }
258 } else {
259 children.len()
260 };
261
262 for child in &children[..end_index] {
263 match child {
264 rowan::NodeOrToken::Node(n) => {
265 builder.start_node(n.kind());
266 Self::add_green_node_children(&mut builder, n);
267 builder.finish_node();
268 }
269 rowan::NodeOrToken::Token(t) => {
270 builder.token(t.kind(), t.text());
271 }
272 }
273 }
274
275 builder.finish_node(); builder.finish_node(); Document(SyntaxNode::new_root_mut(builder.finish()))
280 }
281
282 fn add_green_node_children(builder: &mut GreenNodeBuilder, node: &rowan::GreenNodeData) {
284 for child in node.children() {
285 match child {
286 rowan::NodeOrToken::Node(n) => {
287 builder.start_node(n.kind());
288 Self::add_green_node_children(builder, n);
289 builder.finish_node();
290 }
291 rowan::NodeOrToken::Token(t) => {
292 builder.token(t.kind(), t.text());
293 }
294 }
295 }
296 }
297
298 pub fn insert_after(
307 &self,
308 after_key: impl crate::AsYaml,
309 key: impl crate::AsYaml,
310 value: impl crate::AsYaml,
311 ) -> bool {
312 if let Some(mapping) = self.as_mapping() {
313 mapping.insert_after(after_key, key, value)
314 } else {
315 false
316 }
317 }
318
319 pub fn move_after(
328 &self,
329 after_key: impl crate::AsYaml,
330 key: impl crate::AsYaml,
331 value: impl crate::AsYaml,
332 ) -> bool {
333 if let Some(mapping) = self.as_mapping() {
334 mapping.move_after(after_key, key, value)
335 } else {
336 false
337 }
338 }
339
340 pub fn insert_before(
349 &self,
350 before_key: impl crate::AsYaml,
351 key: impl crate::AsYaml,
352 value: impl crate::AsYaml,
353 ) -> bool {
354 if let Some(mapping) = self.as_mapping() {
355 mapping.insert_before(before_key, key, value)
356 } else {
357 false
358 }
359 }
360
361 pub fn move_before(
370 &self,
371 before_key: impl crate::AsYaml,
372 key: impl crate::AsYaml,
373 value: impl crate::AsYaml,
374 ) -> bool {
375 if let Some(mapping) = self.as_mapping() {
376 mapping.move_before(before_key, key, value)
377 } else {
378 false
379 }
380 }
381
382 pub(crate) fn build_value_content(
384 builder: &mut GreenNodeBuilder,
385 value: impl crate::AsYaml,
386 indent: usize,
387 ) {
388 builder.start_node(SyntaxKind::VALUE.into());
389 value.build_content(builder, indent, false);
390 builder.finish_node(); }
392
393 pub fn insert_at_index(
399 &self,
400 index: usize,
401 key: impl crate::AsYaml,
402 value: impl crate::AsYaml,
403 ) {
404 if let Some(mapping) = self.as_mapping() {
406 mapping.insert_at_index(index, key, value);
407 return;
408 }
409
410 let mapping = Mapping::new();
412 mapping.insert_at_index_preserving(index, key, value);
413 let new_doc = Self::from_mapping(mapping);
414 let new_children: Vec<_> = new_doc.0.children_with_tokens().collect();
415 let child_count = self.0.children_with_tokens().count();
416 self.0.splice_children(0..child_count, new_children);
417 }
418
419 pub fn get_string(&self, key: impl crate::AsYaml) -> Option<String> {
427 let content = self.get_node(key)?;
430 if let Some(tagged_node) = TaggedNode::cast(content.clone()) {
431 tagged_node.value().map(|s| s.as_string())
432 } else {
433 Scalar::cast(content).map(|s| s.as_string())
435 }
436 }
437
438 pub fn len(&self) -> usize {
442 self.as_mapping().map(|m| m.len()).unwrap_or(0)
443 }
444
445 pub fn get_mapping(&self, key: impl crate::AsYaml) -> Option<Mapping> {
449 self.get(key).and_then(|n| n.as_mapping().cloned())
450 }
451
452 pub fn get_sequence(&self, key: impl crate::AsYaml) -> Option<Sequence> {
456 self.get(key).and_then(|n| n.as_sequence().cloned())
457 }
458
459 pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
464 self.as_mapping()
465 .map(|m| m.rename_key(old_key, new_key))
466 .unwrap_or(false)
467 }
468
469 pub fn is_sequence(&self, key: impl crate::AsYaml) -> bool {
471 self.get(key)
472 .map(|node| node.as_sequence().is_some())
473 .unwrap_or(false)
474 }
475
476 pub fn reorder_fields<I, K>(&self, order: I)
481 where
482 I: IntoIterator<Item = K>,
483 K: crate::AsYaml,
484 {
485 if let Some(mapping) = self.as_mapping() {
486 mapping.reorder_fields(order);
487 }
488 }
489
490 pub fn validate_schema(
518 &self,
519 validator: &crate::schema::SchemaValidator,
520 ) -> crate::schema::ValidationResult<()> {
521 validator.validate(self)
522 }
523
524 pub fn byte_range(&self) -> crate::TextPosition {
540 self.0.text_range().into()
541 }
542
543 pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
565 let range = self.byte_range();
566 crate::byte_offset_to_line_column(source_text, range.start as usize)
567 }
568
569 pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
578 let range = self.byte_range();
579 crate::byte_offset_to_line_column(source_text, range.end as usize)
580 }
581
582 pub fn comments(&self) -> impl Iterator<Item = super::Comment> {
594 super::comment::comments(&self.0)
595 }
596}
597
598impl Default for Document {
599 fn default() -> Self {
600 Self::new()
601 }
602}
603
604impl std::str::FromStr for Document {
605 type Err = crate::error::YamlError;
606
607 fn from_str(s: &str) -> Result<Self, Self::Err> {
621 let parsed = YamlFile::parse(s);
622
623 if !parsed.positioned_errors().is_empty() {
624 let first_error = &parsed.positioned_errors()[0];
625 let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
626 return Err(crate::error::YamlError::Parse {
627 message: first_error.message.clone(),
628 line: Some(lc.line),
629 column: Some(lc.column),
630 });
631 }
632
633 let mut docs = parsed.tree().documents();
634 let first = docs.next().unwrap_or_default();
635
636 if docs.next().is_some() {
637 return Err(crate::error::YamlError::InvalidOperation {
638 operation: "Document::from_str".to_string(),
639 reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
640 });
641 }
642
643 Ok(first)
644 }
645}
646
647impl AsYaml for Document {
648 fn as_node(&self) -> Option<&SyntaxNode> {
649 Some(&self.0)
650 }
651
652 fn kind(&self) -> YamlKind {
653 YamlKind::Document
654 }
655
656 fn build_content(
657 &self,
658 builder: &mut rowan::GreenNodeBuilder,
659 _indent: usize,
660 _flow_context: bool,
661 ) -> bool {
662 crate::as_yaml::copy_node_content(builder, &self.0);
663 self.0
664 .last_token()
665 .map(|t| t.kind() == SyntaxKind::NEWLINE)
666 .unwrap_or(false)
667 }
668
669 fn is_inline(&self) -> bool {
670 false
672 }
673}
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use crate::builder::{MappingBuilder, SequenceBuilder};
678 use crate::yaml::YamlFile;
679 use std::str::FromStr;
680
681 #[test]
682 fn test_document_stream_features() {
683 let yaml1 = "---\ndoc1: first\n---\ndoc2: second\n...\n";
685 let parsed1 = YamlFile::from_str(yaml1).unwrap();
686 assert_eq!(parsed1.documents().count(), 2);
687 assert_eq!(parsed1.to_string(), yaml1);
688
689 let yaml2 = "---\nkey: value\n...\n";
691 let parsed2 = YamlFile::from_str(yaml2).unwrap();
692 assert_eq!(parsed2.documents().count(), 1);
693 assert_eq!(parsed2.to_string(), yaml2);
694
695 let yaml3 = "key: value\n...\n";
697 let parsed3 = YamlFile::from_str(yaml3).unwrap();
698 assert_eq!(parsed3.documents().count(), 1);
699 assert_eq!(parsed3.to_string(), yaml3);
700 }
701 #[test]
702 fn test_document_level_directives() {
703 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";
705 let parsed = YamlFile::from_str(yaml).unwrap();
706 assert_eq!(parsed.documents().count(), 2);
707 assert_eq!(parsed.to_string(), yaml);
708 }
709 #[test]
710 fn test_document_schema_validation_api() {
711 let json_yaml = r#"
715name: "John"
716age: 30
717active: true
718items:
719 - "apple"
720 - 42
721 - true
722"#;
723 let doc = YamlFile::from_str(json_yaml).unwrap().document().unwrap();
724
725 assert!(
727 crate::schema::SchemaValidator::json()
728 .validate(&doc)
729 .is_ok(),
730 "JSON-compatible document should pass JSON validation"
731 );
732
733 assert!(
735 crate::schema::SchemaValidator::core()
736 .validate(&doc)
737 .is_ok(),
738 "Valid document should pass Core validation"
739 );
740
741 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
744 assert!(
745 doc.validate_schema(&failsafe_strict).is_err(),
746 "Document with numbers and booleans should fail strict Failsafe validation"
747 );
748
749 let yaml_specific = r#"
751name: "Test"
752created: 2023-12-25T10:30:45Z
753pattern: !!regex '\d+'
754data: !!binary "SGVsbG8="
755"#;
756 let yaml_doc = YamlFile::from_str(yaml_specific)
757 .unwrap()
758 .document()
759 .unwrap();
760
761 assert!(
763 crate::schema::SchemaValidator::core()
764 .validate(&yaml_doc)
765 .is_ok(),
766 "YAML-specific types should pass Core validation"
767 );
768
769 assert!(
771 crate::schema::SchemaValidator::json()
772 .validate(&yaml_doc)
773 .is_err(),
774 "YAML-specific types should fail JSON validation"
775 );
776
777 assert!(
779 crate::schema::SchemaValidator::failsafe()
780 .validate(&yaml_doc)
781 .is_err(),
782 "YAML-specific types should fail Failsafe validation"
783 );
784
785 let string_only = r#"
787name: hello
788message: world
789items:
790 - apple
791 - banana
792nested:
793 key: value
794"#;
795 let str_doc = YamlFile::from_str(string_only).unwrap().document().unwrap();
796
797 assert!(
799 crate::schema::SchemaValidator::failsafe()
800 .validate(&str_doc)
801 .is_ok(),
802 "String-only document should pass Failsafe validation"
803 );
804 assert!(
805 crate::schema::SchemaValidator::json()
806 .validate(&str_doc)
807 .is_ok(),
808 "String-only document should pass JSON validation"
809 );
810 assert!(
811 crate::schema::SchemaValidator::core()
812 .validate(&str_doc)
813 .is_ok(),
814 "String-only document should pass Core validation"
815 );
816 }
817 #[test]
818 fn test_document_set_preserves_position() {
819 let yaml = r#"Name: original
821Version: 1.0
822Author: Someone
823"#;
824 let parsed = YamlFile::from_str(yaml).unwrap();
825 let doc = parsed.document().expect("Should have a document");
826
827 doc.set("Version", 2.0);
829
830 let output = doc.to_string();
831 let expected = r#"Name: original
832Version: 2.0
833Author: Someone
834"#;
835 assert_eq!(output, expected);
836 }
837 #[test]
838 fn test_document_schema_coercion_api() {
839 let coercion_yaml = r#"
841count: "42"
842enabled: "true"
843rate: "3.14"
844items:
845 - "100"
846 - "false"
847"#;
848 let doc = YamlFile::from_str(coercion_yaml)
849 .unwrap()
850 .document()
851 .unwrap();
852 let json_validator = crate::schema::SchemaValidator::json();
853
854 assert!(
856 json_validator.can_coerce(&doc).is_ok(),
857 "Strings that look like numbers/booleans should be coercible to JSON types"
858 );
859
860 let non_coercible = r#"
862timestamp: !!timestamp "2023-01-01"
863pattern: !!regex '\d+'
864"#;
865 let non_coer_doc = YamlFile::from_str(non_coercible)
866 .unwrap()
867 .document()
868 .unwrap();
869
870 assert!(
872 json_validator.can_coerce(&non_coer_doc).is_err(),
873 "YAML-specific types should not be coercible to JSON schema"
874 );
875 }
876 #[test]
877 fn test_document_schema_validation_errors() {
878 let nested_yaml = r#"
880users:
881 - name: "Alice"
882 age: 25
883 metadata:
884 created: !!timestamp "2023-01-01"
885 active: true
886 - name: "Bob"
887 score: 95.5
888"#;
889 let doc = YamlFile::from_str(nested_yaml).unwrap().document().unwrap();
890
891 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
893 let failsafe_result = doc.validate_schema(&failsafe_strict);
894 assert!(
895 failsafe_result.is_err(),
896 "Nested document with numbers should fail strict Failsafe validation"
897 );
898
899 let errors = failsafe_result.unwrap_err();
900 assert!(!errors.is_empty());
901
902 for error in &errors {
904 assert!(!error.path.is_empty(), "Error should have path: {}", error);
905 }
906
907 let json_result = crate::schema::SchemaValidator::json().validate(&doc);
909 assert!(
910 json_result.is_err(),
911 "Document with timestamp should fail JSON validation"
912 );
913
914 let json_errors = json_result.unwrap_err();
915 assert!(!json_errors.is_empty());
916 assert!(
918 json_errors.iter().any(|e| e.schema_name == "json"),
919 "Should have JSON schema validation error"
920 );
921 }
922 #[test]
923 fn test_document_schema_validation_with_custom_validator() {
924 let yaml = r#"
926name: "HelloWorld"
927count: 42
928active: true
929"#;
930 let doc = YamlFile::from_str(yaml).unwrap().document().unwrap();
931
932 let json_validator = crate::schema::SchemaValidator::json();
934 let core_validator = crate::schema::SchemaValidator::core();
935
936 assert!(
938 doc.validate_schema(&core_validator).is_ok(),
939 "Should pass Core validation"
940 );
941 assert!(
942 doc.validate_schema(&json_validator).is_ok(),
943 "Should pass JSON validation"
944 );
945 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
947 assert!(
948 doc.validate_schema(&failsafe_strict).is_err(),
949 "Should fail strict Failsafe validation"
950 );
951
952 let strict_json = crate::schema::SchemaValidator::json().strict();
954 assert!(
956 doc.validate_schema(&strict_json).is_ok(),
957 "Should pass strict JSON validation (integers and booleans are JSON-compatible)"
958 );
959
960 let strict_failsafe = crate::schema::SchemaValidator::failsafe().strict();
961 assert!(
962 doc.validate_schema(&strict_failsafe).is_err(),
963 "Should fail strict Failsafe validation"
964 );
965 }
966 #[test]
967 fn test_document_level_insertion_with_complex_types() {
968 let doc1 = Document::new();
972 doc1.set("name", "project");
973 let features = SequenceBuilder::new()
974 .item("auth")
975 .item("api")
976 .item("web")
977 .build_document()
978 .as_sequence()
979 .unwrap();
980 let success = doc1.insert_after("name", "features", features);
981 assert!(success);
982 let output1 = doc1.to_string();
983 assert_eq!(
984 output1,
985 "---\nname: project\nfeatures:\n - auth\n - api\n - web\n"
986 );
987
988 let doc2 = Document::new();
990 doc2.set("name", "project");
991 doc2.set("version", "1.0.0");
992 let database = MappingBuilder::new()
993 .pair("host", "localhost")
994 .pair("port", 5432)
995 .build_document()
996 .as_mapping()
997 .unwrap();
998 let success = doc2.insert_before("version", "database", database);
999 assert!(success);
1000 let output2 = doc2.to_string();
1001 assert_eq!(
1002 output2,
1003 "---\nname: project\ndatabase:\n host: localhost\n port: 5432\nversion: 1.0.0\n"
1004 );
1005
1006 let doc3 = Document::new();
1008 doc3.set("name", "project");
1009 #[allow(clippy::disallowed_types)]
1011 let tag_set = {
1012 use crate::value::YamlValue;
1013 let mut tags = std::collections::BTreeSet::new();
1014 tags.insert("production".to_string());
1015 tags.insert("database".to_string());
1016 YamlValue::from_set(tags)
1017 };
1018 doc3.insert_at_index(1, "tags", tag_set);
1019 let output3 = doc3.to_string();
1020 assert_eq!(
1021 output3,
1022 "---\nname: project\ntags: !!set\n database: null\n production: null\n"
1023 );
1024
1025 assert!(
1027 YamlFile::from_str(&output1).is_ok(),
1028 "Sequence output should be valid YAML"
1029 );
1030 assert!(
1031 YamlFile::from_str(&output2).is_ok(),
1032 "Mapping output should be valid YAML"
1033 );
1034 assert!(
1035 YamlFile::from_str(&output3).is_ok(),
1036 "Set output should be valid YAML"
1037 );
1038 }
1039
1040 #[test]
1041 fn test_document_api_usage() -> crate::error::YamlResult<()> {
1042 let doc = Document::new();
1044
1045 assert!(!doc.contains_key("Repository"));
1047 doc.set("Repository", "https://github.com/user/repo.git");
1048 assert!(doc.contains_key("Repository"));
1049
1050 assert_eq!(
1052 doc.get_string("Repository"),
1053 Some("https://github.com/user/repo.git".to_string())
1054 );
1055
1056 assert!(!doc.is_empty());
1058
1059 let keys: Vec<_> = doc.keys().collect();
1061 assert_eq!(keys.len(), 1);
1062
1063 assert!(doc.remove("Repository").is_some());
1065 assert!(!doc.contains_key("Repository"));
1066 assert!(doc.is_empty());
1067
1068 Ok(())
1069 }
1070
1071 #[test]
1072 fn test_field_ordering() {
1073 let doc = Document::new();
1074
1075 doc.set("Repository-Browse", "https://github.com/user/repo");
1077 doc.set("Name", "MyProject");
1078 doc.set("Bug-Database", "https://github.com/user/repo/issues");
1079 doc.set("Repository", "https://github.com/user/repo.git");
1080
1081 doc.reorder_fields(["Name", "Bug-Database", "Repository", "Repository-Browse"]);
1083
1084 let keys: Vec<_> = doc.keys().collect();
1086 assert_eq!(keys.len(), 4);
1087 assert_eq!(
1088 keys[0].as_scalar().map(|s| s.as_string()),
1089 Some("Name".to_string())
1090 );
1091 assert_eq!(
1092 keys[1].as_scalar().map(|s| s.as_string()),
1093 Some("Bug-Database".to_string())
1094 );
1095 assert_eq!(
1096 keys[2].as_scalar().map(|s| s.as_string()),
1097 Some("Repository".to_string())
1098 );
1099 assert_eq!(
1100 keys[3].as_scalar().map(|s| s.as_string()),
1101 Some("Repository-Browse".to_string())
1102 );
1103 }
1104
1105 #[test]
1106 fn test_array_detection() {
1107 use crate::scalar::ScalarValue;
1108
1109 let doc = Document::new();
1111
1112 let array_value = SequenceBuilder::new()
1114 .item(ScalarValue::string("https://github.com/user/repo.git"))
1115 .item(ScalarValue::string("https://gitlab.com/user/repo.git"))
1116 .build_document()
1117 .as_sequence()
1118 .unwrap();
1119 doc.set("Repository", &array_value);
1120
1121 assert!(doc.is_sequence("Repository"));
1123 assert!(doc.get_sequence("Repository").is_some());
1126 }
1127
1128 #[test]
1129 fn test_file_io() -> crate::error::YamlResult<()> {
1130 use std::fs;
1131
1132 let test_path = "/tmp/test_yaml_edit.yaml";
1134
1135 let doc = Document::new();
1137 doc.set("Name", "TestProject");
1138 doc.set("Repository", "https://example.com/repo.git");
1139
1140 doc.to_file(test_path)?;
1141
1142 let loaded_doc = Document::from_file(test_path)?;
1144
1145 assert_eq!(
1146 loaded_doc.get_string("Name"),
1147 Some("TestProject".to_string())
1148 );
1149 assert_eq!(
1150 loaded_doc.get_string("Repository"),
1151 Some("https://example.com/repo.git".to_string())
1152 );
1153
1154 let _ = fs::remove_file(test_path);
1156
1157 Ok(())
1158 }
1159
1160 #[test]
1161 fn test_document_from_str_single_document() {
1162 let yaml = "key: value\nport: 8080";
1164 let doc = Document::from_str(yaml).unwrap();
1165
1166 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1167 assert!(doc.contains_key("port"));
1168 }
1169
1170 #[test]
1171 fn test_document_from_str_multiple_documents_error() {
1172 let yaml = "---\nkey: value\n---\nother: data";
1174 let result = Document::from_str(yaml);
1175
1176 assert!(result.is_err());
1177 let err = result.unwrap_err();
1178 match err {
1179 crate::error::YamlError::InvalidOperation { operation, reason } => {
1180 assert_eq!(operation, "Document::from_str");
1181 assert_eq!(
1182 reason,
1183 "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML."
1184 );
1185 }
1186 _ => panic!("Expected InvalidOperation error, got {:?}", err),
1187 }
1188 }
1189
1190 #[test]
1191 fn test_document_from_str_empty() {
1192 let yaml = "";
1194 let doc = Document::from_str(yaml).unwrap();
1195
1196 assert!(doc.is_empty());
1198 }
1199
1200 #[test]
1201 fn test_document_from_str_bare_document() {
1202 let yaml = "name: test\nversion: 1.0";
1204 let doc = Document::from_str(yaml).unwrap();
1205
1206 assert_eq!(doc.get_string("name"), Some("test".to_string()));
1207 assert_eq!(doc.get_string("version"), Some("1.0".to_string()));
1208 }
1209
1210 #[test]
1211 fn test_document_from_str_with_explicit_marker() {
1212 let yaml = "---\nkey: value";
1214 let doc = Document::from_str(yaml).unwrap();
1215
1216 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1217 }
1218
1219 #[test]
1220 fn test_document_from_str_complex_structure() {
1221 let yaml = r#"
1223database:
1224 host: localhost
1225 port: 5432
1226 credentials:
1227 username: admin
1228 password: secret
1229features:
1230 - auth
1231 - logging
1232 - metrics
1233"#;
1234 let doc = Document::from_str(yaml).unwrap();
1235
1236 assert!(doc.contains_key("database"));
1238 assert!(doc.contains_key("features"));
1239
1240 let db = doc.get("database").unwrap();
1242 if let Some(db_map) = db.as_mapping() {
1243 assert!(db_map.contains_key("host"));
1244 } else {
1245 panic!("Expected mapping for database");
1246 }
1247 }
1248
1249 #[test]
1250 fn test_is_sequence_block() {
1251 let doc = Document::from_str("tags:\n - alpha\n - beta\n - gamma").unwrap();
1252 assert!(doc.is_sequence("tags"));
1253 assert_eq!(
1254 doc.get_sequence("tags")
1255 .and_then(|s| s.get(0))
1256 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1257 Some("alpha".to_string())
1258 );
1259 }
1260
1261 #[test]
1262 fn test_is_sequence_flow() {
1263 let doc = Document::from_str("tags: [alpha, beta, gamma]").unwrap();
1264 assert!(doc.is_sequence("tags"));
1265 assert_eq!(
1266 doc.get_sequence("tags")
1267 .and_then(|s| s.get(0))
1268 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1269 Some("alpha".to_string())
1270 );
1271 }
1272
1273 #[test]
1274 fn test_sequence_first_element_flow_quoted_with_comma() {
1275 let doc = Document::from_str("tags: [\"hello, world\", beta]").unwrap();
1277 assert_eq!(
1278 doc.get_sequence("tags")
1279 .and_then(|s| s.get(0))
1280 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1281 Some("hello, world".to_string())
1282 );
1283 }
1284
1285 #[test]
1286 fn test_is_sequence_missing_key() {
1287 let doc = Document::from_str("name: test").unwrap();
1288 assert!(!doc.is_sequence("tags"));
1289 }
1290
1291 #[test]
1292 fn test_is_sequence_not_sequence() {
1293 let doc = Document::from_str("name: test").unwrap();
1294 assert!(!doc.is_sequence("name"));
1295 }
1296
1297 #[test]
1298 fn test_is_sequence_empty_sequence() {
1299 let doc = Document::from_str("tags: []").unwrap();
1300 assert!(doc.is_sequence("tags"));
1301 assert_eq!(doc.get_sequence("tags").map(|s| s.len()), Some(0));
1302 }
1303
1304 #[test]
1305 fn test_get_string_plain_scalar() {
1306 let doc = Document::from_str("key: hello").unwrap();
1307 assert_eq!(doc.get_string("key"), Some("hello".to_string()));
1308 }
1309
1310 #[test]
1311 fn test_get_string_double_quoted_with_escapes() {
1312 let doc = Document::from_str(r#"key: "hello \"world\"""#).unwrap();
1313 assert_eq!(doc.get_string("key"), Some(r#"hello "world""#.to_string()));
1314 }
1315
1316 #[test]
1317 fn test_get_string_single_quoted() {
1318 let doc = Document::from_str("key: 'it''s fine'").unwrap();
1319 assert_eq!(doc.get_string("key"), Some("it's fine".to_string()));
1320 }
1321
1322 #[test]
1323 fn test_get_string_missing_key() {
1324 let doc = Document::from_str("other: value").unwrap();
1325 assert_eq!(doc.get_string("key"), None);
1326 }
1327
1328 #[test]
1329 fn test_get_string_sequence_value_returns_none() {
1330 let doc = Document::from_str("key:\n - a\n - b").unwrap();
1331 assert_eq!(doc.get_string("key"), None);
1332 }
1333
1334 #[test]
1335 fn test_get_string_mapping_value_returns_none() {
1336 let doc = Document::from_str("key:\n nested: value").unwrap();
1337 assert_eq!(doc.get_string("key"), None);
1338 }
1339
1340 #[test]
1341 fn test_insert_after_preserves_newline() {
1342 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new\n";
1344 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1345
1346 if let Some(doc) = yaml_obj.document() {
1348 let result = doc.insert_after(
1349 "Bug-Submit",
1350 "Repository",
1351 "https://github.com/example/example.git",
1352 );
1353 assert!(result, "insert_after should return true when key is found");
1354
1355 let output = doc.to_string();
1357
1358 let expected = "---
1359Bug-Database: https://github.com/example/example/issues
1360Bug-Submit: https://github.com/example/example/issues/new
1361Repository: https://github.com/example/example.git
1362";
1363 assert_eq!(output, expected);
1364 }
1365 }
1366
1367 #[test]
1368 fn test_insert_after_without_trailing_newline() {
1369 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new";
1371 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1372
1373 if let Some(doc) = yaml_obj.document() {
1374 let result = doc.insert_after(
1375 "Bug-Submit",
1376 "Repository",
1377 "https://github.com/example/example.git",
1378 );
1379 assert!(result, "insert_after should return true when key is found");
1380
1381 let output = doc.to_string();
1382
1383 let expected = "---
1384Bug-Database: https://github.com/example/example/issues
1385Bug-Submit: https://github.com/example/example/issues/new
1386Repository: https://github.com/example/example.git
1387";
1388 assert_eq!(output, expected);
1389 }
1390 }
1391
1392 #[test]
1393 fn test_from_str_relaxed_valid() {
1394 let (doc, errors) = Document::from_str_relaxed("key: value\n");
1395 assert!(errors.is_empty());
1396 let mapping = doc.as_mapping().unwrap();
1397 assert_eq!(
1398 mapping.get("key").unwrap().as_scalar().unwrap().to_string(),
1399 "value"
1400 );
1401 }
1402
1403 #[test]
1404 fn test_from_str_relaxed_with_errors() {
1405 let (doc, errors) = Document::from_str_relaxed("key: [unclosed");
1406 assert!(!errors.is_empty());
1407 assert!(doc.as_mapping().is_some());
1409 }
1410
1411 #[test]
1412 fn test_from_str_relaxed_multi_document() {
1413 let (doc, errors) = Document::from_str_relaxed("a: 1\n---\nb: 2\n");
1415 assert!(errors.is_empty());
1416 let mapping = doc.as_mapping().unwrap();
1417 assert_eq!(
1418 mapping.get("a").unwrap().as_scalar().unwrap().to_string(),
1419 "1"
1420 );
1421 }
1422
1423 #[test]
1424 fn test_from_str_relaxed_empty_input() {
1425 let (doc, errors) = Document::from_str_relaxed("");
1426 assert!(errors.is_empty());
1427 assert!(doc.as_mapping().is_none());
1429 }
1430}