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
583impl Default for Document {
584 fn default() -> Self {
585 Self::new()
586 }
587}
588
589impl std::str::FromStr for Document {
590 type Err = crate::error::YamlError;
591
592 fn from_str(s: &str) -> Result<Self, Self::Err> {
606 let parsed = YamlFile::parse(s);
607
608 if !parsed.positioned_errors().is_empty() {
609 let first_error = &parsed.positioned_errors()[0];
610 let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
611 return Err(crate::error::YamlError::Parse {
612 message: first_error.message.clone(),
613 line: Some(lc.line),
614 column: Some(lc.column),
615 });
616 }
617
618 let mut docs = parsed.tree().documents();
619 let first = docs.next().unwrap_or_default();
620
621 if docs.next().is_some() {
622 return Err(crate::error::YamlError::InvalidOperation {
623 operation: "Document::from_str".to_string(),
624 reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
625 });
626 }
627
628 Ok(first)
629 }
630}
631
632impl AsYaml for Document {
633 fn as_node(&self) -> Option<&SyntaxNode> {
634 Some(&self.0)
635 }
636
637 fn kind(&self) -> YamlKind {
638 YamlKind::Document
639 }
640
641 fn build_content(
642 &self,
643 builder: &mut rowan::GreenNodeBuilder,
644 _indent: usize,
645 _flow_context: bool,
646 ) -> bool {
647 crate::as_yaml::copy_node_content(builder, &self.0);
648 self.0
649 .last_token()
650 .map(|t| t.kind() == SyntaxKind::NEWLINE)
651 .unwrap_or(false)
652 }
653
654 fn is_inline(&self) -> bool {
655 false
657 }
658}
659#[cfg(test)]
660mod tests {
661 use super::*;
662 use crate::builder::{MappingBuilder, SequenceBuilder};
663 use crate::yaml::YamlFile;
664 use std::str::FromStr;
665
666 #[test]
667 fn test_document_stream_features() {
668 let yaml1 = "---\ndoc1: first\n---\ndoc2: second\n...\n";
670 let parsed1 = YamlFile::from_str(yaml1).unwrap();
671 assert_eq!(parsed1.documents().count(), 2);
672 assert_eq!(parsed1.to_string(), yaml1);
673
674 let yaml2 = "---\nkey: value\n...\n";
676 let parsed2 = YamlFile::from_str(yaml2).unwrap();
677 assert_eq!(parsed2.documents().count(), 1);
678 assert_eq!(parsed2.to_string(), yaml2);
679
680 let yaml3 = "key: value\n...\n";
682 let parsed3 = YamlFile::from_str(yaml3).unwrap();
683 assert_eq!(parsed3.documents().count(), 1);
684 assert_eq!(parsed3.to_string(), yaml3);
685 }
686 #[test]
687 fn test_document_level_directives() {
688 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";
690 let parsed = YamlFile::from_str(yaml).unwrap();
691 assert_eq!(parsed.documents().count(), 2);
692 assert_eq!(parsed.to_string(), yaml);
693 }
694 #[test]
695 fn test_document_schema_validation_api() {
696 let json_yaml = r#"
700name: "John"
701age: 30
702active: true
703items:
704 - "apple"
705 - 42
706 - true
707"#;
708 let doc = YamlFile::from_str(json_yaml).unwrap().document().unwrap();
709
710 assert!(
712 crate::schema::SchemaValidator::json()
713 .validate(&doc)
714 .is_ok(),
715 "JSON-compatible document should pass JSON validation"
716 );
717
718 assert!(
720 crate::schema::SchemaValidator::core()
721 .validate(&doc)
722 .is_ok(),
723 "Valid document should pass Core validation"
724 );
725
726 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
729 assert!(
730 doc.validate_schema(&failsafe_strict).is_err(),
731 "Document with numbers and booleans should fail strict Failsafe validation"
732 );
733
734 let yaml_specific = r#"
736name: "Test"
737created: 2023-12-25T10:30:45Z
738pattern: !!regex '\d+'
739data: !!binary "SGVsbG8="
740"#;
741 let yaml_doc = YamlFile::from_str(yaml_specific)
742 .unwrap()
743 .document()
744 .unwrap();
745
746 assert!(
748 crate::schema::SchemaValidator::core()
749 .validate(&yaml_doc)
750 .is_ok(),
751 "YAML-specific types should pass Core validation"
752 );
753
754 assert!(
756 crate::schema::SchemaValidator::json()
757 .validate(&yaml_doc)
758 .is_err(),
759 "YAML-specific types should fail JSON validation"
760 );
761
762 assert!(
764 crate::schema::SchemaValidator::failsafe()
765 .validate(&yaml_doc)
766 .is_err(),
767 "YAML-specific types should fail Failsafe validation"
768 );
769
770 let string_only = r#"
772name: hello
773message: world
774items:
775 - apple
776 - banana
777nested:
778 key: value
779"#;
780 let str_doc = YamlFile::from_str(string_only).unwrap().document().unwrap();
781
782 assert!(
784 crate::schema::SchemaValidator::failsafe()
785 .validate(&str_doc)
786 .is_ok(),
787 "String-only document should pass Failsafe validation"
788 );
789 assert!(
790 crate::schema::SchemaValidator::json()
791 .validate(&str_doc)
792 .is_ok(),
793 "String-only document should pass JSON validation"
794 );
795 assert!(
796 crate::schema::SchemaValidator::core()
797 .validate(&str_doc)
798 .is_ok(),
799 "String-only document should pass Core validation"
800 );
801 }
802 #[test]
803 fn test_document_set_preserves_position() {
804 let yaml = r#"Name: original
806Version: 1.0
807Author: Someone
808"#;
809 let parsed = YamlFile::from_str(yaml).unwrap();
810 let doc = parsed.document().expect("Should have a document");
811
812 doc.set("Version", 2.0);
814
815 let output = doc.to_string();
816 let expected = r#"Name: original
817Version: 2.0
818Author: Someone
819"#;
820 assert_eq!(output, expected);
821 }
822 #[test]
823 fn test_document_schema_coercion_api() {
824 let coercion_yaml = r#"
826count: "42"
827enabled: "true"
828rate: "3.14"
829items:
830 - "100"
831 - "false"
832"#;
833 let doc = YamlFile::from_str(coercion_yaml)
834 .unwrap()
835 .document()
836 .unwrap();
837 let json_validator = crate::schema::SchemaValidator::json();
838
839 assert!(
841 json_validator.can_coerce(&doc).is_ok(),
842 "Strings that look like numbers/booleans should be coercible to JSON types"
843 );
844
845 let non_coercible = r#"
847timestamp: !!timestamp "2023-01-01"
848pattern: !!regex '\d+'
849"#;
850 let non_coer_doc = YamlFile::from_str(non_coercible)
851 .unwrap()
852 .document()
853 .unwrap();
854
855 assert!(
857 json_validator.can_coerce(&non_coer_doc).is_err(),
858 "YAML-specific types should not be coercible to JSON schema"
859 );
860 }
861 #[test]
862 fn test_document_schema_validation_errors() {
863 let nested_yaml = r#"
865users:
866 - name: "Alice"
867 age: 25
868 metadata:
869 created: !!timestamp "2023-01-01"
870 active: true
871 - name: "Bob"
872 score: 95.5
873"#;
874 let doc = YamlFile::from_str(nested_yaml).unwrap().document().unwrap();
875
876 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
878 let failsafe_result = doc.validate_schema(&failsafe_strict);
879 assert!(
880 failsafe_result.is_err(),
881 "Nested document with numbers should fail strict Failsafe validation"
882 );
883
884 let errors = failsafe_result.unwrap_err();
885 assert!(!errors.is_empty());
886
887 for error in &errors {
889 assert!(!error.path.is_empty(), "Error should have path: {}", error);
890 }
891
892 let json_result = crate::schema::SchemaValidator::json().validate(&doc);
894 assert!(
895 json_result.is_err(),
896 "Document with timestamp should fail JSON validation"
897 );
898
899 let json_errors = json_result.unwrap_err();
900 assert!(!json_errors.is_empty());
901 assert!(
903 json_errors.iter().any(|e| e.schema_name == "json"),
904 "Should have JSON schema validation error"
905 );
906 }
907 #[test]
908 fn test_document_schema_validation_with_custom_validator() {
909 let yaml = r#"
911name: "HelloWorld"
912count: 42
913active: true
914"#;
915 let doc = YamlFile::from_str(yaml).unwrap().document().unwrap();
916
917 let json_validator = crate::schema::SchemaValidator::json();
919 let core_validator = crate::schema::SchemaValidator::core();
920
921 assert!(
923 doc.validate_schema(&core_validator).is_ok(),
924 "Should pass Core validation"
925 );
926 assert!(
927 doc.validate_schema(&json_validator).is_ok(),
928 "Should pass JSON validation"
929 );
930 let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
932 assert!(
933 doc.validate_schema(&failsafe_strict).is_err(),
934 "Should fail strict Failsafe validation"
935 );
936
937 let strict_json = crate::schema::SchemaValidator::json().strict();
939 assert!(
941 doc.validate_schema(&strict_json).is_ok(),
942 "Should pass strict JSON validation (integers and booleans are JSON-compatible)"
943 );
944
945 let strict_failsafe = crate::schema::SchemaValidator::failsafe().strict();
946 assert!(
947 doc.validate_schema(&strict_failsafe).is_err(),
948 "Should fail strict Failsafe validation"
949 );
950 }
951 #[test]
952 fn test_document_level_insertion_with_complex_types() {
953 let doc1 = Document::new();
957 doc1.set("name", "project");
958 let features = SequenceBuilder::new()
959 .item("auth")
960 .item("api")
961 .item("web")
962 .build_document()
963 .as_sequence()
964 .unwrap();
965 let success = doc1.insert_after("name", "features", features);
966 assert!(success);
967 let output1 = doc1.to_string();
968 assert_eq!(
969 output1,
970 "---\nname: project\nfeatures:\n - auth\n - api\n - web\n"
971 );
972
973 let doc2 = Document::new();
975 doc2.set("name", "project");
976 doc2.set("version", "1.0.0");
977 let database = MappingBuilder::new()
978 .pair("host", "localhost")
979 .pair("port", 5432)
980 .build_document()
981 .as_mapping()
982 .unwrap();
983 let success = doc2.insert_before("version", "database", database);
984 assert!(success);
985 let output2 = doc2.to_string();
986 assert_eq!(
987 output2,
988 "---\nname: project\ndatabase:\n host: localhost\n port: 5432\nversion: 1.0.0\n"
989 );
990
991 let doc3 = Document::new();
993 doc3.set("name", "project");
994 #[allow(clippy::disallowed_types)]
996 let tag_set = {
997 use crate::value::YamlValue;
998 let mut tags = std::collections::BTreeSet::new();
999 tags.insert("production".to_string());
1000 tags.insert("database".to_string());
1001 YamlValue::from_set(tags)
1002 };
1003 doc3.insert_at_index(1, "tags", tag_set);
1004 let output3 = doc3.to_string();
1005 assert_eq!(
1006 output3,
1007 "---\nname: project\ntags: !!set\n database: null\n production: null\n"
1008 );
1009
1010 assert!(
1012 YamlFile::from_str(&output1).is_ok(),
1013 "Sequence output should be valid YAML"
1014 );
1015 assert!(
1016 YamlFile::from_str(&output2).is_ok(),
1017 "Mapping output should be valid YAML"
1018 );
1019 assert!(
1020 YamlFile::from_str(&output3).is_ok(),
1021 "Set output should be valid YAML"
1022 );
1023 }
1024
1025 #[test]
1026 fn test_document_api_usage() -> crate::error::YamlResult<()> {
1027 let doc = Document::new();
1029
1030 assert!(!doc.contains_key("Repository"));
1032 doc.set("Repository", "https://github.com/user/repo.git");
1033 assert!(doc.contains_key("Repository"));
1034
1035 assert_eq!(
1037 doc.get_string("Repository"),
1038 Some("https://github.com/user/repo.git".to_string())
1039 );
1040
1041 assert!(!doc.is_empty());
1043
1044 let keys: Vec<_> = doc.keys().collect();
1046 assert_eq!(keys.len(), 1);
1047
1048 assert!(doc.remove("Repository").is_some());
1050 assert!(!doc.contains_key("Repository"));
1051 assert!(doc.is_empty());
1052
1053 Ok(())
1054 }
1055
1056 #[test]
1057 fn test_field_ordering() {
1058 let doc = Document::new();
1059
1060 doc.set("Repository-Browse", "https://github.com/user/repo");
1062 doc.set("Name", "MyProject");
1063 doc.set("Bug-Database", "https://github.com/user/repo/issues");
1064 doc.set("Repository", "https://github.com/user/repo.git");
1065
1066 doc.reorder_fields(["Name", "Bug-Database", "Repository", "Repository-Browse"]);
1068
1069 let keys: Vec<_> = doc.keys().collect();
1071 assert_eq!(keys.len(), 4);
1072 assert_eq!(
1073 keys[0].as_scalar().map(|s| s.as_string()),
1074 Some("Name".to_string())
1075 );
1076 assert_eq!(
1077 keys[1].as_scalar().map(|s| s.as_string()),
1078 Some("Bug-Database".to_string())
1079 );
1080 assert_eq!(
1081 keys[2].as_scalar().map(|s| s.as_string()),
1082 Some("Repository".to_string())
1083 );
1084 assert_eq!(
1085 keys[3].as_scalar().map(|s| s.as_string()),
1086 Some("Repository-Browse".to_string())
1087 );
1088 }
1089
1090 #[test]
1091 fn test_array_detection() {
1092 use crate::scalar::ScalarValue;
1093
1094 let doc = Document::new();
1096
1097 let array_value = SequenceBuilder::new()
1099 .item(ScalarValue::string("https://github.com/user/repo.git"))
1100 .item(ScalarValue::string("https://gitlab.com/user/repo.git"))
1101 .build_document()
1102 .as_sequence()
1103 .unwrap();
1104 doc.set("Repository", &array_value);
1105
1106 assert!(doc.is_sequence("Repository"));
1108 assert!(doc.get_sequence("Repository").is_some());
1111 }
1112
1113 #[test]
1114 fn test_file_io() -> crate::error::YamlResult<()> {
1115 use std::fs;
1116
1117 let test_path = "/tmp/test_yaml_edit.yaml";
1119
1120 let doc = Document::new();
1122 doc.set("Name", "TestProject");
1123 doc.set("Repository", "https://example.com/repo.git");
1124
1125 doc.to_file(test_path)?;
1126
1127 let loaded_doc = Document::from_file(test_path)?;
1129
1130 assert_eq!(
1131 loaded_doc.get_string("Name"),
1132 Some("TestProject".to_string())
1133 );
1134 assert_eq!(
1135 loaded_doc.get_string("Repository"),
1136 Some("https://example.com/repo.git".to_string())
1137 );
1138
1139 let _ = fs::remove_file(test_path);
1141
1142 Ok(())
1143 }
1144
1145 #[test]
1146 fn test_document_from_str_single_document() {
1147 let yaml = "key: value\nport: 8080";
1149 let doc = Document::from_str(yaml).unwrap();
1150
1151 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1152 assert!(doc.contains_key("port"));
1153 }
1154
1155 #[test]
1156 fn test_document_from_str_multiple_documents_error() {
1157 let yaml = "---\nkey: value\n---\nother: data";
1159 let result = Document::from_str(yaml);
1160
1161 assert!(result.is_err());
1162 let err = result.unwrap_err();
1163 match err {
1164 crate::error::YamlError::InvalidOperation { operation, reason } => {
1165 assert_eq!(operation, "Document::from_str");
1166 assert_eq!(
1167 reason,
1168 "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML."
1169 );
1170 }
1171 _ => panic!("Expected InvalidOperation error, got {:?}", err),
1172 }
1173 }
1174
1175 #[test]
1176 fn test_document_from_str_empty() {
1177 let yaml = "";
1179 let doc = Document::from_str(yaml).unwrap();
1180
1181 assert!(doc.is_empty());
1183 }
1184
1185 #[test]
1186 fn test_document_from_str_bare_document() {
1187 let yaml = "name: test\nversion: 1.0";
1189 let doc = Document::from_str(yaml).unwrap();
1190
1191 assert_eq!(doc.get_string("name"), Some("test".to_string()));
1192 assert_eq!(doc.get_string("version"), Some("1.0".to_string()));
1193 }
1194
1195 #[test]
1196 fn test_document_from_str_with_explicit_marker() {
1197 let yaml = "---\nkey: value";
1199 let doc = Document::from_str(yaml).unwrap();
1200
1201 assert_eq!(doc.get_string("key"), Some("value".to_string()));
1202 }
1203
1204 #[test]
1205 fn test_document_from_str_complex_structure() {
1206 let yaml = r#"
1208database:
1209 host: localhost
1210 port: 5432
1211 credentials:
1212 username: admin
1213 password: secret
1214features:
1215 - auth
1216 - logging
1217 - metrics
1218"#;
1219 let doc = Document::from_str(yaml).unwrap();
1220
1221 assert!(doc.contains_key("database"));
1223 assert!(doc.contains_key("features"));
1224
1225 let db = doc.get("database").unwrap();
1227 if let Some(db_map) = db.as_mapping() {
1228 assert!(db_map.contains_key("host"));
1229 } else {
1230 panic!("Expected mapping for database");
1231 }
1232 }
1233
1234 #[test]
1235 fn test_is_sequence_block() {
1236 let doc = Document::from_str("tags:\n - alpha\n - beta\n - gamma").unwrap();
1237 assert!(doc.is_sequence("tags"));
1238 assert_eq!(
1239 doc.get_sequence("tags")
1240 .and_then(|s| s.get(0))
1241 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1242 Some("alpha".to_string())
1243 );
1244 }
1245
1246 #[test]
1247 fn test_is_sequence_flow() {
1248 let doc = Document::from_str("tags: [alpha, beta, gamma]").unwrap();
1249 assert!(doc.is_sequence("tags"));
1250 assert_eq!(
1251 doc.get_sequence("tags")
1252 .and_then(|s| s.get(0))
1253 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1254 Some("alpha".to_string())
1255 );
1256 }
1257
1258 #[test]
1259 fn test_sequence_first_element_flow_quoted_with_comma() {
1260 let doc = Document::from_str("tags: [\"hello, world\", beta]").unwrap();
1262 assert_eq!(
1263 doc.get_sequence("tags")
1264 .and_then(|s| s.get(0))
1265 .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1266 Some("hello, world".to_string())
1267 );
1268 }
1269
1270 #[test]
1271 fn test_is_sequence_missing_key() {
1272 let doc = Document::from_str("name: test").unwrap();
1273 assert!(!doc.is_sequence("tags"));
1274 }
1275
1276 #[test]
1277 fn test_is_sequence_not_sequence() {
1278 let doc = Document::from_str("name: test").unwrap();
1279 assert!(!doc.is_sequence("name"));
1280 }
1281
1282 #[test]
1283 fn test_is_sequence_empty_sequence() {
1284 let doc = Document::from_str("tags: []").unwrap();
1285 assert!(doc.is_sequence("tags"));
1286 assert_eq!(doc.get_sequence("tags").map(|s| s.len()), Some(0));
1287 }
1288
1289 #[test]
1290 fn test_get_string_plain_scalar() {
1291 let doc = Document::from_str("key: hello").unwrap();
1292 assert_eq!(doc.get_string("key"), Some("hello".to_string()));
1293 }
1294
1295 #[test]
1296 fn test_get_string_double_quoted_with_escapes() {
1297 let doc = Document::from_str(r#"key: "hello \"world\"""#).unwrap();
1298 assert_eq!(doc.get_string("key"), Some(r#"hello "world""#.to_string()));
1299 }
1300
1301 #[test]
1302 fn test_get_string_single_quoted() {
1303 let doc = Document::from_str("key: 'it''s fine'").unwrap();
1304 assert_eq!(doc.get_string("key"), Some("it's fine".to_string()));
1305 }
1306
1307 #[test]
1308 fn test_get_string_missing_key() {
1309 let doc = Document::from_str("other: value").unwrap();
1310 assert_eq!(doc.get_string("key"), None);
1311 }
1312
1313 #[test]
1314 fn test_get_string_sequence_value_returns_none() {
1315 let doc = Document::from_str("key:\n - a\n - b").unwrap();
1316 assert_eq!(doc.get_string("key"), None);
1317 }
1318
1319 #[test]
1320 fn test_get_string_mapping_value_returns_none() {
1321 let doc = Document::from_str("key:\n nested: value").unwrap();
1322 assert_eq!(doc.get_string("key"), None);
1323 }
1324
1325 #[test]
1326 fn test_insert_after_preserves_newline() {
1327 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new\n";
1329 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1330
1331 if let Some(doc) = yaml_obj.document() {
1333 let result = doc.insert_after(
1334 "Bug-Submit",
1335 "Repository",
1336 "https://github.com/example/example.git",
1337 );
1338 assert!(result, "insert_after should return true when key is found");
1339
1340 let output = doc.to_string();
1342
1343 let expected = "---
1344Bug-Database: https://github.com/example/example/issues
1345Bug-Submit: https://github.com/example/example/issues/new
1346Repository: https://github.com/example/example.git
1347";
1348 assert_eq!(output, expected);
1349 }
1350 }
1351
1352 #[test]
1353 fn test_insert_after_without_trailing_newline() {
1354 let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new";
1356 let yaml_obj = YamlFile::from_str(yaml).unwrap();
1357
1358 if let Some(doc) = yaml_obj.document() {
1359 let result = doc.insert_after(
1360 "Bug-Submit",
1361 "Repository",
1362 "https://github.com/example/example.git",
1363 );
1364 assert!(result, "insert_after should return true when key is found");
1365
1366 let output = doc.to_string();
1367
1368 let expected = "---
1369Bug-Database: https://github.com/example/example/issues
1370Bug-Submit: https://github.com/example/example/issues/new
1371Repository: https://github.com/example/example.git
1372";
1373 assert_eq!(output, expected);
1374 }
1375 }
1376
1377 #[test]
1378 fn test_from_str_relaxed_valid() {
1379 let (doc, errors) = Document::from_str_relaxed("key: value\n");
1380 assert!(errors.is_empty());
1381 let mapping = doc.as_mapping().unwrap();
1382 assert_eq!(
1383 mapping.get("key").unwrap().as_scalar().unwrap().to_string(),
1384 "value"
1385 );
1386 }
1387
1388 #[test]
1389 fn test_from_str_relaxed_with_errors() {
1390 let (doc, errors) = Document::from_str_relaxed("key: [unclosed");
1391 assert!(!errors.is_empty());
1392 assert!(doc.as_mapping().is_some());
1394 }
1395
1396 #[test]
1397 fn test_from_str_relaxed_multi_document() {
1398 let (doc, errors) = Document::from_str_relaxed("a: 1\n---\nb: 2\n");
1400 assert!(errors.is_empty());
1401 let mapping = doc.as_mapping().unwrap();
1402 assert_eq!(
1403 mapping.get("a").unwrap().as_scalar().unwrap().to_string(),
1404 "1"
1405 );
1406 }
1407
1408 #[test]
1409 fn test_from_str_relaxed_empty_input() {
1410 let (doc, errors) = Document::from_str_relaxed("");
1411 assert!(errors.is_empty());
1412 assert!(doc.as_mapping().is_none());
1414 }
1415}