Skip to main content

yaml_edit/
builder.rs

1//! Builder pattern for constructing YAML documents fluently.
2
3use crate::{
4    as_yaml::{AsYaml, YamlKind},
5    lex::SyntaxKind,
6    yaml::{Document, YamlFile},
7};
8use rowan::GreenNodeBuilder;
9
10/// A builder for constructing YAML documents with a fluent API.
11pub struct YamlBuilder {
12    file: YamlFile,
13}
14
15impl YamlBuilder {
16    /// Start building from a scalar value.
17    pub fn scalar(value: impl AsYaml) -> Self {
18        let mut builder = GreenNodeBuilder::new();
19        builder.start_node(SyntaxKind::ROOT.into());
20        builder.start_node(SyntaxKind::DOCUMENT.into());
21        value.build_content(&mut builder, 0, false);
22        builder.finish_node();
23        builder.finish_node();
24        let green = builder.finish();
25        YamlBuilder {
26            file: YamlFile(rowan::SyntaxNode::new_root_mut(green)),
27        }
28    }
29
30    /// Start building from a sequence.
31    pub fn sequence() -> SequenceBuilder {
32        SequenceBuilder::new()
33    }
34
35    /// Start building from a mapping.
36    pub fn mapping() -> MappingBuilder {
37        MappingBuilder::new()
38    }
39
40    /// Build the final YAML file.
41    pub fn build(self) -> YamlFile {
42        self.file
43    }
44}
45
46impl Default for YamlBuilder {
47    fn default() -> Self {
48        Self::mapping().build()
49    }
50}
51
52/// Builder for YAML sequences.
53pub struct SequenceBuilder {
54    builder: GreenNodeBuilder<'static>,
55    indent: usize,
56    count: usize,
57    /// Whether the last item ended with a newline
58    last_item_ended_with_newline: bool,
59}
60
61impl SequenceBuilder {
62    /// Create a new empty sequence builder.
63    pub fn new() -> Self {
64        let mut builder = GreenNodeBuilder::new();
65        builder.start_node(SyntaxKind::ROOT.into());
66        builder.start_node(SyntaxKind::DOCUMENT.into());
67        builder.start_node(SyntaxKind::SEQUENCE.into());
68        SequenceBuilder {
69            builder,
70            indent: 0,
71            count: 0,
72            last_item_ended_with_newline: false,
73        }
74    }
75
76    fn at_indent(builder: GreenNodeBuilder<'static>, indent: usize) -> Self {
77        SequenceBuilder {
78            builder,
79            indent,
80            count: 0,
81            last_item_ended_with_newline: false,
82        }
83    }
84
85    fn emit_item_preamble(&mut self) {
86        // Only add newline if previous item didn't already end with one
87        if self.count > 0 && !self.last_item_ended_with_newline {
88            self.builder.token(SyntaxKind::NEWLINE.into(), "\n");
89        }
90        if self.indent > 0 {
91            self.builder
92                .token(SyntaxKind::WHITESPACE.into(), &" ".repeat(self.indent));
93        }
94        self.builder.token(SyntaxKind::DASH.into(), "-");
95        self.builder.token(SyntaxKind::WHITESPACE.into(), " ");
96    }
97
98    /// Add a value to the sequence. Accepts any type implementing [`AsYaml`]:
99    /// `&str`, `String`, `i64`, `bool`, `f64`, CST nodes, etc.
100    pub fn item(mut self, value: impl AsYaml) -> Self {
101        self.emit_item_preamble();
102
103        // Check the kind of value to determine formatting
104        let ends_with_newline = match (value.is_inline(), value.kind()) {
105            // Inline values (scalars, flow collections) go on same line
106            (true, _) => value.build_content(&mut self.builder, self.indent, false),
107            // Block mappings and sequences start on same line as dash
108            // Their content will handle indentation via copy_node_content_with_indent
109            (false, YamlKind::Mapping) | (false, YamlKind::Sequence) => {
110                value.build_content(&mut self.builder, self.indent + 2, false)
111            }
112            // Block scalars (literal/folded) need newline before them
113            (false, _) => {
114                self.builder.token(SyntaxKind::NEWLINE.into(), "\n");
115                value.build_content(&mut self.builder, self.indent + 2, false)
116            }
117        };
118
119        self.count += 1;
120        self.last_item_ended_with_newline = ends_with_newline;
121        self
122    }
123
124    /// Add a nested sequence to this sequence.
125    pub fn sequence<F>(self, f: F) -> Self
126    where
127        F: FnOnce(SequenceBuilder) -> SequenceBuilder,
128    {
129        let SequenceBuilder {
130            mut builder,
131            indent,
132            count,
133            ..
134        } = self;
135
136        if count > 0 {
137            builder.token(SyntaxKind::NEWLINE.into(), "\n");
138        }
139        if indent > 0 {
140            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
141        }
142        builder.token(SyntaxKind::DASH.into(), "-");
143        builder.token(SyntaxKind::WHITESPACE.into(), " ");
144        builder.token(SyntaxKind::NEWLINE.into(), "\n");
145
146        builder.start_node(SyntaxKind::SEQUENCE.into());
147        let nested = SequenceBuilder::at_indent(builder, indent + 2);
148        let filled = f(nested);
149        let SequenceBuilder { mut builder, .. } = filled;
150        builder.finish_node(); // SEQUENCE
151
152        SequenceBuilder {
153            builder,
154            indent,
155            count: count + 1,
156            last_item_ended_with_newline: true,
157        }
158    }
159
160    /// Add a nested mapping to this sequence.
161    pub fn mapping<F>(self, f: F) -> Self
162    where
163        F: FnOnce(MappingBuilder) -> MappingBuilder,
164    {
165        let SequenceBuilder {
166            mut builder,
167            indent,
168            count,
169            ..
170        } = self;
171
172        if count > 0 {
173            builder.token(SyntaxKind::NEWLINE.into(), "\n");
174        }
175        if indent > 0 {
176            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
177        }
178        builder.token(SyntaxKind::DASH.into(), "-");
179        builder.token(SyntaxKind::WHITESPACE.into(), " ");
180        builder.token(SyntaxKind::NEWLINE.into(), "\n");
181
182        builder.start_node(SyntaxKind::MAPPING.into());
183        let nested = MappingBuilder::at_indent(builder, indent + 2);
184        let filled = f(nested);
185        let MappingBuilder { mut builder, .. } = filled;
186        builder.finish_node(); // MAPPING
187
188        SequenceBuilder {
189            builder,
190            indent,
191            count: count + 1,
192            last_item_ended_with_newline: true,
193        }
194    }
195
196    /// Insert a pre-built SequenceBuilder into this sequence.
197    pub fn insert_sequence(self, other: SequenceBuilder) -> Self {
198        // Extract the inner SEQUENCE node from the other builder
199        let SequenceBuilder {
200            builder: mut other_builder,
201            ..
202        } = other;
203        other_builder.finish_node(); // SEQUENCE
204        other_builder.finish_node(); // DOCUMENT
205        other_builder.finish_node(); // ROOT
206        let green = other_builder.finish();
207        let root = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
208
209        // Find the SEQUENCE node
210        use rowan::ast::AstNode;
211        if let Some(doc) = crate::yaml::Document::cast(root.first_child().unwrap()) {
212            if let Some(seq_node) = doc.syntax().children().next() {
213                let SequenceBuilder {
214                    mut builder,
215                    indent,
216                    count,
217                    ..
218                } = self;
219
220                if count > 0 {
221                    builder.token(SyntaxKind::NEWLINE.into(), "\n");
222                }
223                if indent > 0 {
224                    builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
225                }
226                builder.token(SyntaxKind::DASH.into(), "-");
227                builder.token(SyntaxKind::WHITESPACE.into(), " ");
228                builder.token(SyntaxKind::NEWLINE.into(), "\n");
229
230                crate::as_yaml::copy_node_content(&mut builder, &seq_node);
231
232                return SequenceBuilder {
233                    builder,
234                    indent,
235                    count: count + 1,
236                    last_item_ended_with_newline: true,
237                };
238            }
239        }
240        self
241    }
242
243    /// Insert a pre-built MappingBuilder into this sequence.
244    pub fn insert_mapping(self, other: MappingBuilder) -> Self {
245        // Extract the inner MAPPING node from the other builder
246        let MappingBuilder {
247            builder: mut other_builder,
248            ..
249        } = other;
250        other_builder.finish_node(); // MAPPING
251        other_builder.finish_node(); // DOCUMENT
252        other_builder.finish_node(); // ROOT
253        let green = other_builder.finish();
254        let root = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
255
256        // Find the MAPPING node
257        use rowan::ast::AstNode;
258        if let Some(doc) = crate::yaml::Document::cast(root.first_child().unwrap()) {
259            if let Some(map_node) = doc.syntax().children().next() {
260                let SequenceBuilder {
261                    mut builder,
262                    indent,
263                    count,
264                    ..
265                } = self;
266
267                if count > 0 {
268                    builder.token(SyntaxKind::NEWLINE.into(), "\n");
269                }
270                if indent > 0 {
271                    builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
272                }
273                builder.token(SyntaxKind::DASH.into(), "-");
274                builder.token(SyntaxKind::WHITESPACE.into(), " ");
275                builder.token(SyntaxKind::NEWLINE.into(), "\n");
276
277                crate::as_yaml::copy_node_content(&mut builder, &map_node);
278
279                return SequenceBuilder {
280                    builder,
281                    indent,
282                    count: count + 1,
283                    last_item_ended_with_newline: true,
284                };
285            }
286        }
287        self
288    }
289
290    /// Build the sequence into a YamlBuilder.
291    pub fn build(mut self) -> YamlBuilder {
292        self.builder.finish_node(); // SEQUENCE
293        self.builder.finish_node(); // DOCUMENT
294        self.builder.finish_node(); // ROOT
295        let green = self.builder.finish();
296        YamlBuilder {
297            file: YamlFile(rowan::SyntaxNode::new_root_mut(green)),
298        }
299    }
300
301    /// Build the sequence directly into a Document.
302    pub fn build_document(self) -> Document {
303        self.build()
304            .build()
305            .document()
306            .expect("YamlBuilder always produces a document node")
307    }
308}
309
310impl Default for SequenceBuilder {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316/// Builder for YAML mappings.
317pub struct MappingBuilder {
318    builder: GreenNodeBuilder<'static>,
319    indent: usize,
320    count: usize,
321}
322
323impl MappingBuilder {
324    /// Create a new empty mapping builder.
325    pub fn new() -> Self {
326        let mut builder = GreenNodeBuilder::new();
327        builder.start_node(SyntaxKind::ROOT.into());
328        builder.start_node(SyntaxKind::DOCUMENT.into());
329        builder.start_node(SyntaxKind::MAPPING.into());
330        MappingBuilder {
331            builder,
332            indent: 0,
333            count: 0,
334        }
335    }
336
337    fn at_indent(builder: GreenNodeBuilder<'static>, indent: usize) -> Self {
338        MappingBuilder {
339            builder,
340            indent,
341            count: 0,
342        }
343    }
344
345    fn emit_key_preamble(&mut self, key: &str) {
346        if self.count > 0 {
347            self.builder.token(SyntaxKind::NEWLINE.into(), "\n");
348        }
349        if self.indent > 0 {
350            self.builder
351                .token(SyntaxKind::WHITESPACE.into(), &" ".repeat(self.indent));
352        }
353        self.builder.start_node(SyntaxKind::SCALAR.into());
354        self.builder.token(SyntaxKind::VALUE.into(), key);
355        self.builder.finish_node();
356        self.builder.token(SyntaxKind::COLON.into(), ":");
357        self.builder.token(SyntaxKind::WHITESPACE.into(), " ");
358    }
359
360    /// Add a key-value pair. The value can be any type implementing [`AsYaml`]:
361    /// `&str`, `String`, `i64`, `bool`, `f64`, CST nodes, etc.
362    pub fn pair(mut self, key: impl Into<String>, value: impl AsYaml) -> Self {
363        self.emit_key_preamble(&key.into());
364        if value.is_inline() {
365            value.build_content(&mut self.builder, self.indent, false);
366        } else {
367            self.builder.token(SyntaxKind::NEWLINE.into(), "\n");
368            value.build_content(&mut self.builder, self.indent + 2, false);
369        }
370        self.count += 1;
371        self
372    }
373
374    /// Add a key-value pair with a sequence value.
375    pub fn sequence<F>(self, key: impl Into<String>, f: F) -> Self
376    where
377        F: FnOnce(SequenceBuilder) -> SequenceBuilder,
378    {
379        let MappingBuilder {
380            mut builder,
381            indent,
382            count,
383        } = self;
384
385        if count > 0 {
386            builder.token(SyntaxKind::NEWLINE.into(), "\n");
387        }
388        if indent > 0 {
389            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
390        }
391        builder.start_node(SyntaxKind::SCALAR.into());
392        builder.token(SyntaxKind::VALUE.into(), &key.into());
393        builder.finish_node();
394        builder.token(SyntaxKind::COLON.into(), ":");
395        builder.token(SyntaxKind::WHITESPACE.into(), " ");
396        builder.token(SyntaxKind::NEWLINE.into(), "\n");
397
398        builder.start_node(SyntaxKind::SEQUENCE.into());
399        let nested = SequenceBuilder::at_indent(builder, indent + 2);
400        let filled = f(nested);
401        let SequenceBuilder { mut builder, .. } = filled;
402        builder.finish_node(); // SEQUENCE
403
404        MappingBuilder {
405            builder,
406            indent,
407            count: count + 1,
408        }
409    }
410
411    /// Add a key-value pair with a mapping value.
412    pub fn mapping<F>(self, key: impl Into<String>, f: F) -> Self
413    where
414        F: FnOnce(MappingBuilder) -> MappingBuilder,
415    {
416        let MappingBuilder {
417            mut builder,
418            indent,
419            count,
420        } = self;
421
422        if count > 0 {
423            builder.token(SyntaxKind::NEWLINE.into(), "\n");
424        }
425        if indent > 0 {
426            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
427        }
428        builder.start_node(SyntaxKind::SCALAR.into());
429        builder.token(SyntaxKind::VALUE.into(), &key.into());
430        builder.finish_node();
431        builder.token(SyntaxKind::COLON.into(), ":");
432        builder.token(SyntaxKind::WHITESPACE.into(), " ");
433        builder.token(SyntaxKind::NEWLINE.into(), "\n");
434
435        builder.start_node(SyntaxKind::MAPPING.into());
436        let nested = MappingBuilder::at_indent(builder, indent + 2);
437        let filled = f(nested);
438        let MappingBuilder { mut builder, .. } = filled;
439        builder.finish_node(); // MAPPING
440
441        MappingBuilder {
442            builder,
443            indent,
444            count: count + 1,
445        }
446    }
447
448    /// Insert a key-value pair with a pre-built SequenceBuilder.
449    pub fn insert_sequence(self, key: impl Into<String>, other: SequenceBuilder) -> Self {
450        // Extract the inner SEQUENCE node from the other builder
451        let SequenceBuilder {
452            builder: mut other_builder,
453            ..
454        } = other;
455        other_builder.finish_node(); // SEQUENCE
456        other_builder.finish_node(); // DOCUMENT
457        other_builder.finish_node(); // ROOT
458        let green = other_builder.finish();
459        let root = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
460
461        // Find the SEQUENCE node
462        use rowan::ast::AstNode;
463        if let Some(doc) = crate::yaml::Document::cast(root.first_child().unwrap()) {
464            if let Some(seq_node) = doc.syntax().children().next() {
465                let MappingBuilder {
466                    mut builder,
467                    indent,
468                    count,
469                } = self;
470
471                if count > 0 {
472                    builder.token(SyntaxKind::NEWLINE.into(), "\n");
473                }
474                if indent > 0 {
475                    builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
476                }
477                builder.start_node(SyntaxKind::SCALAR.into());
478                builder.token(SyntaxKind::VALUE.into(), &key.into());
479                builder.finish_node();
480                builder.token(SyntaxKind::COLON.into(), ":");
481                builder.token(SyntaxKind::WHITESPACE.into(), " ");
482                builder.token(SyntaxKind::NEWLINE.into(), "\n");
483
484                crate::as_yaml::copy_node_content(&mut builder, &seq_node);
485
486                return MappingBuilder {
487                    builder,
488                    indent,
489                    count: count + 1,
490                };
491            }
492        }
493        self
494    }
495
496    /// Insert a key-value pair with a pre-built MappingBuilder.
497    pub fn insert_mapping(self, key: impl Into<String>, other: MappingBuilder) -> Self {
498        // Extract the inner MAPPING node from the other builder
499        let MappingBuilder {
500            builder: mut other_builder,
501            ..
502        } = other;
503        other_builder.finish_node(); // MAPPING
504        other_builder.finish_node(); // DOCUMENT
505        other_builder.finish_node(); // ROOT
506        let green = other_builder.finish();
507        let root = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
508
509        // Find the MAPPING node
510        use rowan::ast::AstNode;
511        if let Some(doc) = crate::yaml::Document::cast(root.first_child().unwrap()) {
512            if let Some(map_node) = doc.syntax().children().next() {
513                let MappingBuilder {
514                    mut builder,
515                    indent,
516                    count,
517                } = self;
518
519                if count > 0 {
520                    builder.token(SyntaxKind::NEWLINE.into(), "\n");
521                }
522                if indent > 0 {
523                    builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
524                }
525                builder.start_node(SyntaxKind::SCALAR.into());
526                builder.token(SyntaxKind::VALUE.into(), &key.into());
527                builder.finish_node();
528                builder.token(SyntaxKind::COLON.into(), ":");
529                builder.token(SyntaxKind::WHITESPACE.into(), " ");
530                builder.token(SyntaxKind::NEWLINE.into(), "\n");
531
532                crate::as_yaml::copy_node_content_with_indent(&mut builder, &map_node, indent + 2);
533
534                return MappingBuilder {
535                    builder,
536                    indent,
537                    count: count + 1,
538                };
539            }
540        }
541        self
542    }
543
544    /// Build the mapping into a YamlBuilder.
545    pub fn build(mut self) -> YamlBuilder {
546        self.builder.finish_node(); // MAPPING
547        self.builder.finish_node(); // DOCUMENT
548        self.builder.finish_node(); // ROOT
549        let green = self.builder.finish();
550        YamlBuilder {
551            file: YamlFile(rowan::SyntaxNode::new_root_mut(green)),
552        }
553    }
554
555    /// Build the mapping directly into a Document.
556    pub fn build_document(self) -> Document {
557        self.build()
558            .build()
559            .document()
560            .expect("YamlBuilder always produces a document node")
561    }
562}
563
564impl Default for MappingBuilder {
565    fn default() -> Self {
566        Self::new()
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use super::*;
573
574    #[test]
575    fn test_scalar_builder() {
576        let yaml = YamlBuilder::scalar("hello world").build();
577        assert_eq!(yaml.to_string(), "hello world");
578    }
579
580    #[test]
581    fn test_sequence_builder() {
582        let yaml = YamlBuilder::sequence()
583            .item("first")
584            .item("second")
585            .item("third")
586            .build()
587            .build();
588        assert_eq!(yaml.to_string(), "- first\n- second\n- third");
589    }
590
591    #[test]
592    fn test_mapping_builder() {
593        let yaml = YamlBuilder::mapping()
594            .pair("name", "John Doe")
595            .pair("age", "30")
596            .pair("city", "New York")
597            .build()
598            .build();
599        assert_eq!(
600            yaml.to_string(),
601            "name: John Doe\nage: '30'\ncity: New York"
602        );
603    }
604
605    #[test]
606    fn test_nested_structure() {
607        let yaml = YamlBuilder::mapping()
608            .pair("version", "1.0")
609            .sequence("dependencies", |s| {
610                s.item("serde").item("tokio").item("reqwest")
611            })
612            .mapping("database", |m| {
613                m.pair("host", "localhost")
614                    .pair("port", "5432")
615                    .pair("name", "myapp")
616            })
617            .build()
618            .build();
619        assert_eq!(
620            yaml.to_string(),
621            "version: '1.0'\ndependencies: \n  - serde\n  - tokio\n  - reqwest\ndatabase: \n  host: localhost\n  port: '5432'\n  name: myapp"
622        );
623    }
624
625    #[test]
626    fn test_deeply_nested() {
627        let yaml = YamlBuilder::mapping()
628            .mapping("level1", |m| {
629                m.mapping("level2", |m| {
630                    m.mapping("level3", |m| m.pair("deep", "value"))
631                })
632            })
633            .build()
634            .build();
635        assert_eq!(
636            yaml.to_string(),
637            "level1: \n  level2: \n    level3: \n      deep: value"
638        );
639    }
640
641    #[test]
642    fn test_empty_collections() {
643        // Empty sequence
644        let empty_seq = YamlBuilder::sequence().build().build();
645        let text = empty_seq.to_string();
646        assert_eq!(text.trim(), "");
647
648        // Empty mapping
649        let empty_map = YamlBuilder::mapping().build().build();
650        let text = empty_map.to_string();
651        assert_eq!(text.trim(), "");
652    }
653
654    #[test]
655    fn test_special_characters_in_values() {
656        let yaml = YamlBuilder::mapping()
657            .pair("url", "https://example.com:8080/path?query=value")
658            .pair("email", "user@example.com")
659            .pair("path", "/usr/local/bin")
660            .pair("special", "value: with: colons")
661            .build()
662            .build();
663        assert_eq!(
664            yaml.to_string(),
665            "url: https://example.com:8080/path?query=value\nemail: user@example.com\npath: /usr/local/bin\nspecial: 'value: with: colons'"
666        );
667    }
668
669    #[test]
670    fn test_numeric_string_values() {
671        let yaml = YamlBuilder::mapping()
672            .pair("int_string", "42")
673            .pair("float_string", "3.14")
674            .pair("hex_string", "0xFF")
675            .pair("octal_string", "0o755")
676            .pair("binary_string", "0b1010")
677            .build()
678            .build();
679        assert_eq!(
680            yaml.to_string(),
681            "int_string: '42'\nfloat_string: '3.14'\nhex_string: '0xFF'\noctal_string: '0o755'\nbinary_string: '0b1010'"
682        );
683    }
684
685    #[test]
686    fn test_sequences_with_nested_mappings() {
687        let yaml = YamlBuilder::sequence()
688            .mapping(|m| m.pair("id", "1").pair("name", "Alice"))
689            .mapping(|m| m.pair("id", "2").pair("name", "Bob"))
690            .mapping(|m| m.pair("id", "3").pair("name", "Charlie"))
691            .build()
692            .build();
693        assert_eq!(
694            yaml.to_string(),
695            "- \n  id: '1'\n  name: Alice\n- \n  id: '2'\n  name: Bob\n- \n  id: '3'\n  name: Charlie"
696        );
697    }
698
699    #[test]
700    fn test_sequences_with_nested_sequences() {
701        let yaml = YamlBuilder::sequence()
702            .sequence(|s| s.item("1").item("2").item("3"))
703            .sequence(|s| s.item("a").item("b").item("c"))
704            .sequence(|s| s.item("x").item("y").item("z"))
705            .build()
706            .build();
707        assert_eq!(
708            yaml.to_string(),
709            "- \n  - '1'\n  - '2'\n  - '3'\n- \n  - a\n  - b\n  - c\n- \n  - x\n  - y\n  - z"
710        );
711    }
712
713    #[test]
714    fn test_mixed_nesting_depth() {
715        let yaml = YamlBuilder::mapping()
716            .sequence("list", |s| {
717                s.item("simple")
718                    .mapping(|m| m.pair("key", "value"))
719                    .sequence(|s2| s2.item("nested1").item("nested2"))
720            })
721            .mapping("object", |m| {
722                m.pair("simple", "value")
723                    .sequence("list", |s| s.item("item1").item("item2"))
724                    .mapping("nested", |m2| m2.pair("deep", "value"))
725            })
726            .build()
727            .build();
728        assert_eq!(
729            yaml.to_string(),
730            "list: \n  - simple\n  - \n    key: value\n  - \n    - nested1\n    - nested2\nobject: \n  simple: value\n  list: \n    - item1\n    - item2\n  nested: \n    deep: value"
731        );
732    }
733
734    #[test]
735    fn test_boolean_and_null_strings() {
736        let yaml = YamlBuilder::mapping()
737            .pair("bool_true", "true")
738            .pair("bool_false", "false")
739            .pair("yes", "yes")
740            .pair("no", "no")
741            .pair("null_value", "null")
742            .pair("tilde", "~")
743            .build()
744            .build();
745        assert_eq!(
746            yaml.to_string(),
747            "bool_true: 'true'\nbool_false: 'false'\nyes: 'yes'\nno: 'no'\nnull_value: 'null'\ntilde: '~'"
748        );
749    }
750
751    #[test]
752    fn test_long_strings() {
753        let yaml = YamlBuilder::mapping()
754            .pair("short", "test")
755            .pair("long", "a".repeat(100))
756            .build()
757            .build();
758        assert_eq!(
759            yaml.to_string(),
760            format!("short: test\nlong: {}", "a".repeat(100))
761        );
762    }
763
764    #[test]
765    fn test_unicode_values() {
766        let yaml = YamlBuilder::mapping()
767            .pair("emoji", "🎉🚀💻")
768            .pair("chinese", "你好世界")
769            .pair("arabic", "مرحبا بالعالم")
770            .pair("mixed", "Hello 世界 🌍")
771            .build()
772            .build();
773        assert_eq!(
774            yaml.to_string(),
775            "emoji: 🎉🚀💻\nchinese: 你好世界\narabic: مرحبا بالعالم\nmixed: Hello 世界 🌍"
776        );
777    }
778
779    #[test]
780    fn test_build_document_convenience_sequence() {
781        let doc = YamlBuilder::sequence()
782            .item("first")
783            .item("second")
784            .build_document();
785
786        let text = doc.to_string();
787        assert_eq!(text.trim(), "- first\n- second");
788    }
789
790    #[test]
791    fn test_build_document_convenience_mapping() {
792        let doc = YamlBuilder::mapping()
793            .pair("name", "test")
794            .pair("version", "1.0")
795            .build_document();
796
797        let text = doc.to_string();
798        assert_eq!(text.trim(), "name: test\nversion: '1.0'");
799    }
800
801    #[test]
802    fn test_insert_pre_built_sequence() {
803        // Use closure-based API instead of insert_sequence for proper indentation
804        let doc = YamlBuilder::mapping()
805            .pair("name", "my-app")
806            .sequence("dependencies", |s| s.item("serde").item("tokio"))
807            .build_document();
808
809        let text = doc.to_string();
810        assert_eq!(
811            text.trim(),
812            "name: my-app\ndependencies: \n  - serde\n  - tokio"
813        );
814    }
815
816    #[test]
817    fn test_insert_pre_built_mapping() {
818        // Use closure-based API instead of insert_mapping for proper indentation
819        let doc = YamlBuilder::mapping()
820            .pair("name", "my-app")
821            .mapping("database", |m| {
822                m.pair("host", "localhost").pair("port", 5432)
823            })
824            .build_document();
825
826        let text = doc.to_string();
827        assert_eq!(
828            text.trim(),
829            "name: my-app\ndatabase: \n  host: localhost\n  port: 5432"
830        );
831    }
832
833    #[test]
834    fn test_insert_in_sequence() {
835        // Use closure-based API for nested collections
836        let doc = YamlBuilder::sequence()
837            .item("first")
838            .sequence(|s| s.item("a").item("b"))
839            .mapping(|m| m.pair("key", "value"))
840            .build_document();
841
842        let text = doc.to_string();
843        assert_eq!(text.trim(), "- first\n- \n  - a\n  - b\n- \n  key: value");
844    }
845
846    #[test]
847    fn test_complex_pre_built_structure() {
848        // Use closure-based API for complex nested structures
849        let doc = YamlBuilder::mapping()
850            .pair("version", "1.0")
851            .pair("name", "my-application")
852            .mapping("database", |m| {
853                m.pair("host", "localhost")
854                    .pair("port", 5432)
855                    .pair("name", "myapp")
856            })
857            .sequence("dependencies", |s| {
858                s.item("serde").item("tokio").item("reqwest")
859            })
860            .build_document();
861
862        let text = doc.to_string();
863        assert_eq!(
864            text.trim(),
865            "version: '1.0'\nname: my-application\ndatabase: \n  host: localhost\n  port: 5432\n  name: myapp\ndependencies: \n  - serde\n  - tokio\n  - reqwest"
866        );
867    }
868
869    #[test]
870    fn test_pair_with_typed_values() {
871        let yaml = YamlBuilder::mapping()
872            .pair("port", 5432_i64)
873            .pair("debug", true)
874            .pair("ratio", 1.5_f64)
875            .build()
876            .build();
877        assert_eq!(yaml.to_string(), "port: 5432\ndebug: true\nratio: 1.5");
878    }
879
880    // Tests from sequence_builder_mapping_formatting.rs
881
882    #[test]
883    fn test_sequence_builder_with_block_mappings() {
884        use crate::Document;
885        use std::str::FromStr;
886
887        // Parse a YAML document with duplicate keys (each has a mapping value)
888        let yaml = r#"
889Reference:
890  Author: Stefan Kurze
891  Title: Wörterbücher und Textdateien durchsuchen mit grafischem Frontend
892  Journal: LinuxUser
893  Year: 2003
894Reference:
895  Author: Michael Vogelbacher
896  Title: Service und Informationen aus dem Netz
897  Journal: LinuxUser
898  Year: 2001
899"#;
900
901        let doc = Document::from_str(yaml).unwrap();
902        let mapping = doc.as_mapping().unwrap();
903
904        // Collect the duplicate Reference values
905        let mut reference_values = Vec::new();
906        for (key, value) in &mapping {
907            if let Some(key_scalar) = key.as_scalar() {
908                if key_scalar.as_string() == "Reference" {
909                    reference_values.push(value);
910                }
911            }
912        }
913
914        // Remove all Reference keys
915        while mapping.remove("Reference").is_some() {}
916
917        // Create a sequence from the collected values
918        let mut seq_builder = SequenceBuilder::new();
919        for value in &reference_values {
920            seq_builder = seq_builder.item(value);
921        }
922        let seq_doc = seq_builder.build_document();
923
924        // Set the sequence back
925        if let Some(seq) = seq_doc.as_sequence() {
926            mapping.set("Reference", seq);
927        }
928
929        let result = doc.to_string();
930
931        // Expected format: each mapping item should start with dash at base indent
932        // and mapping content should be properly indented
933        let expected = r#"Reference:
934- Author: Stefan Kurze
935  Title: Wörterbücher und Textdateien durchsuchen mit grafischem Frontend
936  Journal: LinuxUser
937  Year: 2003
938- Author: Michael Vogelbacher
939  Title: Service und Informationen aus dem Netz
940  Journal: LinuxUser
941  Year: 2001
942"#;
943
944        assert_eq!(result.trim(), expected.trim());
945    }
946
947    #[test]
948    fn test_sequence_builder_simple_mapping() {
949        use crate::Document;
950        use std::str::FromStr;
951
952        let yaml = r#"
953item:
954  key: value
955  foo: bar
956"#;
957
958        let doc = Document::from_str(yaml).unwrap();
959        let mapping = doc.as_mapping().unwrap();
960        let item_value = mapping.get("item").unwrap();
961        let item_mapping = item_value.as_mapping().unwrap();
962
963        // Create a sequence with this mapping
964        let seq = SequenceBuilder::new().item(item_mapping).build_document();
965
966        let result = seq.to_string();
967
968        // Should format as:
969        // - key: value
970        //   foo: bar
971        let expected = "- key: value\n  foo: bar";
972        assert_eq!(result.trim(), expected);
973    }
974}