Skip to main content

yaml_edit/
builder.rs

1//! Builder pattern for constructing YAML documents fluently.
2
3use crate::{
4    as_yaml::{AsYaml, YamlNode},
5    lex::SyntaxKind,
6    yaml::{Document, SyntaxNode, YamlFile},
7};
8use rowan::{ast::AstNode, Checkpoint, 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        if !value.build_content(&mut builder, 0, false) {
22            builder.token(SyntaxKind::NEWLINE.into(), "\n");
23        }
24        builder.finish_node();
25        builder.finish_node();
26        let green = builder.finish();
27        YamlBuilder {
28            file: YamlFile(rowan::SyntaxNode::new_root_mut(green)),
29        }
30    }
31
32    /// Start building from an alias (`*name`).
33    ///
34    /// `name` is the anchor name without the `*` prefix.
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// use yaml_edit::YamlBuilder;
40    ///
41    /// let yaml = YamlBuilder::alias("shared").build();
42    /// assert_eq!(yaml.to_string(), "*shared\n");
43    /// ```
44    pub fn alias(name: impl AsRef<str>) -> Self {
45        Self::scalar(crate::yaml::Alias::new(name))
46    }
47
48    /// Start building from a sequence.
49    pub fn sequence() -> SequenceBuilder {
50        SequenceBuilder::new()
51    }
52
53    /// Start building from a mapping.
54    pub fn mapping() -> MappingBuilder {
55        MappingBuilder::new()
56    }
57
58    /// Build the final YAML file.
59    pub fn build(self) -> YamlFile {
60        self.file
61    }
62}
63
64impl Default for YamlBuilder {
65    fn default() -> Self {
66        Self::mapping().build()
67    }
68}
69
70/// Builder for YAML sequences.
71pub struct SequenceBuilder {
72    builder: GreenNodeBuilder<'static>,
73    /// Column of this sequence's dashes. Only a top-level sequence sits at
74    /// column 0; a nested one starts on the line after its parent's `-` or
75    /// `key:`.
76    indent: usize,
77    /// Where the SEQUENCE node starts, set once the first entry is emitted.
78    start: Option<Checkpoint>,
79}
80
81/// Emit what precedes the next entry of a block collection at column
82/// `indent`, recording in `start` where the collection node begins.
83///
84/// The parser keeps the line break and indent before a nested collection's
85/// first entry in the parent VALUE or SEQUENCE_ENTRY, so `start` comes after
86/// them; later entries get an INDENT sibling inside the collection.
87fn begin_entry(
88    builder: &mut GreenNodeBuilder<'static>,
89    indent: usize,
90    start: &mut Option<Checkpoint>,
91) {
92    if start.is_some() {
93        if indent > 0 {
94            builder.token(SyntaxKind::INDENT.into(), &" ".repeat(indent));
95        }
96        return;
97    }
98    if indent > 0 {
99        builder.token(SyntaxKind::NEWLINE.into(), "\n");
100        builder.token(SyntaxKind::INDENT.into(), &" ".repeat(indent));
101    }
102    *start = Some(builder.checkpoint());
103}
104
105/// Wrap the entries emitted since `start` in a `kind` node, or emit an
106/// empty flow collection if there were none: an empty block collection
107/// serializes as nothing and reparses as null under a key (issue #37).
108fn finish_collection(
109    builder: &mut GreenNodeBuilder<'static>,
110    kind: SyntaxKind,
111    start: Option<Checkpoint>,
112) {
113    let Some(start) = start else {
114        builder.start_node(kind.into());
115        if kind == SyntaxKind::SEQUENCE {
116            builder.token(SyntaxKind::LEFT_BRACKET.into(), "[");
117            builder.token(SyntaxKind::RIGHT_BRACKET.into(), "]");
118        } else {
119            builder.token(SyntaxKind::LEFT_BRACE.into(), "{");
120            builder.token(SyntaxKind::RIGHT_BRACE.into(), "}");
121        }
122        builder.finish_node();
123        return;
124    };
125    builder.start_node_at(start, kind.into());
126    builder.finish_node();
127}
128
129/// Finish a collection filled by a closure after its parent's `key:` or
130/// `-`, terminating the parent entry. A non-empty one already sits on the
131/// following lines; an empty one goes on the same line. In a mapping entry,
132/// `value_start` is where the VALUE node begins, which takes in the line
133/// break before a block collection.
134fn finish_nested(
135    builder: &mut GreenNodeBuilder<'static>,
136    kind: SyntaxKind,
137    start: Option<Checkpoint>,
138    value_start: Option<Checkpoint>,
139) {
140    if start.is_some() {
141        finish_collection(builder, kind, start);
142        if let Some(value_start) = value_start {
143            builder.start_node_at(value_start, SyntaxKind::VALUE.into());
144            builder.finish_node();
145        }
146        builder.finish_node();
147        return;
148    }
149    builder.token(SyntaxKind::WHITESPACE.into(), " ");
150    if value_start.is_some() {
151        builder.start_node(SyntaxKind::VALUE.into());
152    }
153    finish_collection(builder, kind, None);
154    if value_start.is_some() {
155        builder.finish_node();
156    }
157    finish_entry(builder, false);
158}
159
160/// Close the current entry, terminating its line unless its value did.
161fn finish_entry(builder: &mut GreenNodeBuilder<'static>, ends_with_newline: bool) {
162    if !ends_with_newline {
163        builder.token(SyntaxKind::NEWLINE.into(), "\n");
164    }
165    builder.finish_node();
166}
167
168/// Finish a top-level collection and its document.
169fn finish_top_level(
170    mut builder: GreenNodeBuilder<'static>,
171    kind: SyntaxKind,
172    start: Option<Checkpoint>,
173) -> YamlBuilder {
174    let empty = start.is_none();
175    finish_collection(&mut builder, kind, start);
176    // Block entries end their own lines; `[]`/`{}` leaves it to the document.
177    if empty {
178        builder.token(SyntaxKind::NEWLINE.into(), "\n");
179    }
180    builder.finish_node(); // DOCUMENT
181    builder.finish_node(); // ROOT
182    YamlBuilder {
183        file: YamlFile(rowan::SyntaxNode::new_root_mut(builder.finish())),
184    }
185}
186
187/// The collection a builder's [`build`](MappingBuilder::build) produced.
188fn root_node(built: YamlBuilder) -> YamlNode {
189    built
190        .build()
191        .document()
192        .and_then(|d| d.syntax().first_child())
193        .and_then(YamlNode::from_syntax)
194        .expect("builders always produce a document holding a collection")
195}
196
197/// Start a MAPPING_ENTRY and emit its `key:`, quoting the key as needed to
198/// keep it a string.
199fn start_mapping_entry(builder: &mut GreenNodeBuilder<'static>, key: &str) {
200    builder.start_node(SyntaxKind::MAPPING_ENTRY.into());
201    builder.start_node(SyntaxKind::KEY.into());
202    crate::nodes::build_key_content(builder, &key, 0, None);
203    builder.finish_node();
204    builder.token(SyntaxKind::COLON.into(), ":");
205}
206
207/// Emit the children of a VALUE built by
208/// [`build_block_value_node`](crate::nodes::mapping::build_block_value_node),
209/// wrapped in VALUE again if `wrap`, after the space that separates them
210/// from the `:` or `-` if they start on that line. Returns whether they end
211/// with a newline.
212fn emit_block_value(
213    builder: &mut GreenNodeBuilder<'static>,
214    source: &SyntaxNode,
215    target: usize,
216    wrap: bool,
217) -> bool {
218    let block = crate::nodes::mapping::build_block_value_node(source, target, None);
219    if block.on_key_line {
220        builder.token(SyntaxKind::WHITESPACE.into(), " ");
221    }
222    if wrap {
223        crate::yaml::copy_node_to_builder(builder, &block.node);
224    } else {
225        crate::as_yaml::copy_node_content(builder, &block.node);
226    }
227    block.ends_with_newline
228}
229
230impl SequenceBuilder {
231    /// Create a new empty sequence builder.
232    pub fn new() -> Self {
233        let mut builder = GreenNodeBuilder::new();
234        builder.start_node(SyntaxKind::ROOT.into());
235        builder.start_node(SyntaxKind::DOCUMENT.into());
236        Self::nested(builder, 0)
237    }
238
239    fn nested(builder: GreenNodeBuilder<'static>, indent: usize) -> Self {
240        SequenceBuilder {
241            builder,
242            indent,
243            start: None,
244        }
245    }
246
247    /// Emit the next entry's leading whitespace and open it up to its `-`.
248    fn start_entry(&mut self) {
249        begin_entry(&mut self.builder, self.indent, &mut self.start);
250        self.builder.start_node(SyntaxKind::SEQUENCE_ENTRY.into());
251        self.builder.token(SyntaxKind::DASH.into(), "-");
252    }
253
254    /// Add a value to the sequence. Accepts any type implementing [`AsYaml`]:
255    /// `&str`, `String`, `i64`, `bool`, `f64`, CST nodes, etc.
256    pub fn item(mut self, value: impl AsYaml) -> Self {
257        self.start_entry();
258        let nested = self.indent + 2;
259        let block_node = value.value_node().filter(|n| {
260            !value.is_inline() && !matches!(n.kind(), SyntaxKind::MAPPING | SyntaxKind::SEQUENCE)
261        });
262        let ends_with_newline = match block_node {
263            // Block scalars and tagged block content.
264            Some(node) => emit_block_value(&mut self.builder, &node, nested, false),
265            // A block collection starts on the dash line, its later lines
266            // lined up under the first.
267            None => {
268                self.builder.token(SyntaxKind::WHITESPACE.into(), " ");
269                let indent = if value.is_inline() {
270                    self.indent
271                } else {
272                    nested
273                };
274                value.build_content(&mut self.builder, indent, false)
275            }
276        };
277        finish_entry(&mut self.builder, ends_with_newline);
278        self
279    }
280
281    /// Add a nested sequence to this sequence.
282    pub fn sequence<F>(mut self, f: F) -> Self
283    where
284        F: FnOnce(SequenceBuilder) -> SequenceBuilder,
285    {
286        self.start_entry();
287        let filled = f(SequenceBuilder::nested(self.builder, self.indent + 2));
288        self.builder = filled.builder;
289        finish_nested(&mut self.builder, SyntaxKind::SEQUENCE, filled.start, None);
290        self
291    }
292
293    /// Add a nested mapping to this sequence.
294    pub fn mapping<F>(mut self, f: F) -> Self
295    where
296        F: FnOnce(MappingBuilder) -> MappingBuilder,
297    {
298        self.start_entry();
299        let filled = f(MappingBuilder::nested(self.builder, self.indent + 2));
300        self.builder = filled.builder;
301        finish_nested(&mut self.builder, SyntaxKind::MAPPING, filled.start, None);
302        self
303    }
304
305    /// Insert a pre-built SequenceBuilder into this sequence.
306    pub fn insert_sequence(self, other: SequenceBuilder) -> Self {
307        self.insert_node(other.into_node())
308    }
309
310    /// Insert a pre-built MappingBuilder into this sequence.
311    pub fn insert_mapping(self, other: MappingBuilder) -> Self {
312        self.insert_node(other.into_node())
313    }
314
315    /// Add a collection from another builder, laid out the way the
316    /// closure-based methods lay it out: on the lines after the `-`.
317    fn insert_node(mut self, node: YamlNode) -> Self {
318        if node.is_inline() {
319            // `[]` or `{}`
320            return self.item(node);
321        }
322        self.start_entry();
323        let ends_with_newline =
324            emit_block_value(&mut self.builder, node.syntax(), self.indent + 2, false);
325        finish_entry(&mut self.builder, ends_with_newline);
326        self
327    }
328
329    /// Finish this builder on its own and return the SEQUENCE node.
330    fn into_node(self) -> YamlNode {
331        root_node(self.build())
332    }
333
334    /// Build the sequence into a YamlBuilder.
335    pub fn build(self) -> YamlBuilder {
336        finish_top_level(self.builder, SyntaxKind::SEQUENCE, self.start)
337    }
338
339    /// Build the sequence directly into a Document.
340    pub fn build_document(self) -> Document {
341        self.build()
342            .build()
343            .document()
344            .expect("YamlBuilder always produces a document node")
345    }
346}
347
348impl Default for SequenceBuilder {
349    fn default() -> Self {
350        Self::new()
351    }
352}
353
354/// Builder for YAML mappings.
355pub struct MappingBuilder {
356    builder: GreenNodeBuilder<'static>,
357    /// Column of this mapping's keys; see [`SequenceBuilder`].
358    indent: usize,
359    /// Where the MAPPING node starts, set once the first entry is emitted.
360    start: Option<Checkpoint>,
361}
362
363impl MappingBuilder {
364    /// Create a new empty mapping builder.
365    pub fn new() -> Self {
366        let mut builder = GreenNodeBuilder::new();
367        builder.start_node(SyntaxKind::ROOT.into());
368        builder.start_node(SyntaxKind::DOCUMENT.into());
369        Self::nested(builder, 0)
370    }
371
372    fn nested(builder: GreenNodeBuilder<'static>, indent: usize) -> Self {
373        MappingBuilder {
374            builder,
375            indent,
376            start: None,
377        }
378    }
379
380    /// Emit the next entry's leading whitespace and open it up to its `:`.
381    fn start_entry(&mut self, key: &str) {
382        begin_entry(&mut self.builder, self.indent, &mut self.start);
383        start_mapping_entry(&mut self.builder, key);
384    }
385
386    /// Add a key-value pair. The value can be any type implementing [`AsYaml`]:
387    /// `&str`, `String`, `i64`, `bool`, `f64`, CST nodes, etc.
388    ///
389    /// The key is always a string: it is quoted if it would otherwise read
390    /// as another type or not parse as a key at all.
391    pub fn pair(mut self, key: impl Into<String>, value: impl AsYaml) -> Self {
392        self.start_entry(&key.into());
393        let nested = self.indent + 2;
394        let ends_with_newline = match value.value_node().filter(|_| !value.is_inline()) {
395            Some(node) => emit_block_value(&mut self.builder, &node, nested, true),
396            None if value.is_inline() => {
397                self.builder.token(SyntaxKind::WHITESPACE.into(), " ");
398                self.builder.start_node(SyntaxKind::VALUE.into());
399                let ends = value.build_content(&mut self.builder, self.indent, false);
400                self.builder.finish_node();
401                ends
402            }
403            None => {
404                self.builder.start_node(SyntaxKind::VALUE.into());
405                self.builder.token(SyntaxKind::NEWLINE.into(), "\n");
406                self.builder
407                    .token(SyntaxKind::INDENT.into(), &" ".repeat(nested));
408                let ends = value.build_content(&mut self.builder, nested, false);
409                self.builder.finish_node();
410                ends
411            }
412        };
413        finish_entry(&mut self.builder, ends_with_newline);
414        self
415    }
416
417    /// Add a key-value pair with a sequence value.
418    pub fn sequence<F>(mut self, key: impl Into<String>, f: F) -> Self
419    where
420        F: FnOnce(SequenceBuilder) -> SequenceBuilder,
421    {
422        self.start_entry(&key.into());
423        let value_start = self.builder.checkpoint();
424        let filled = f(SequenceBuilder::nested(self.builder, self.indent + 2));
425        self.builder = filled.builder;
426        finish_nested(
427            &mut self.builder,
428            SyntaxKind::SEQUENCE,
429            filled.start,
430            Some(value_start),
431        );
432        self
433    }
434
435    /// Add a key-value pair with a mapping value.
436    pub fn mapping<F>(mut self, key: impl Into<String>, f: F) -> Self
437    where
438        F: FnOnce(MappingBuilder) -> MappingBuilder,
439    {
440        self.start_entry(&key.into());
441        let value_start = self.builder.checkpoint();
442        let filled = f(MappingBuilder::nested(self.builder, self.indent + 2));
443        self.builder = filled.builder;
444        finish_nested(
445            &mut self.builder,
446            SyntaxKind::MAPPING,
447            filled.start,
448            Some(value_start),
449        );
450        self
451    }
452
453    /// Insert a key-value pair with a pre-built SequenceBuilder.
454    pub fn insert_sequence(self, key: impl Into<String>, other: SequenceBuilder) -> Self {
455        self.pair(key, other.into_node())
456    }
457
458    /// Insert a key-value pair with a pre-built MappingBuilder.
459    pub fn insert_mapping(self, key: impl Into<String>, other: MappingBuilder) -> Self {
460        self.pair(key, other.into_node())
461    }
462
463    /// Finish this builder on its own and return the MAPPING node.
464    fn into_node(self) -> YamlNode {
465        root_node(self.build())
466    }
467
468    /// Build the mapping into a YamlBuilder.
469    pub fn build(self) -> YamlBuilder {
470        finish_top_level(self.builder, SyntaxKind::MAPPING, self.start)
471    }
472
473    /// Build the mapping directly into a Document.
474    pub fn build_document(self) -> Document {
475        self.build()
476            .build()
477            .document()
478            .expect("YamlBuilder always produces a document node")
479    }
480}
481
482impl Default for MappingBuilder {
483    fn default() -> Self {
484        Self::new()
485    }
486}