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