Skip to main content

yaml_rt_core/
fragment.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::{self, Write as _};
3
4use crate::{NodeId, SemanticKind, YamlDoc, YamlError, YamlScalarStyle, strip_inline_comment};
5
6/// A parsed YAML value document containing exactly one root node.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct YamlFragment {
9    doc: YamlDoc,
10    root: NodeId,
11}
12
13impl YamlFragment {
14    /// Parses an owned YAML value.
15    ///
16    /// # Errors
17    ///
18    /// Returns an error when the input is invalid YAML, does not contain exactly
19    /// one rooted document, or contains an alias that escapes the value root.
20    pub fn parse_owned(input: String) -> Result<Self, FragmentError> {
21        let doc = YamlDoc::parse_owned(input).map_err(FragmentError::from)?;
22        if doc.document_count() != 1 {
23            return Err(FragmentError::new(format!(
24                "a YAML value must contain exactly one document, found {}",
25                doc.document_count()
26            )));
27        }
28        let root = doc
29            .document_root(0)
30            .map_err(FragmentError::from)?
31            .ok_or_else(|| FragmentError::new("a YAML value must contain one root node"))?;
32        let fragment = Self { doc, root };
33        fragment.validate_alias_scope()?;
34        Ok(fragment)
35    }
36
37    /// Parses a borrowed YAML value.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error under the same conditions as [`Self::parse_owned`].
42    pub fn parse(input: &str) -> Result<Self, FragmentError> {
43        Self::parse_owned(input.to_owned())
44    }
45
46    /// Returns the fragment's parsed document.
47    #[must_use]
48    pub fn document(&self) -> &YamlDoc {
49        &self.doc
50    }
51
52    /// Returns the fragment root node.
53    #[must_use]
54    pub const fn root(&self) -> NodeId {
55        self.root
56    }
57
58    /// Returns the root as minimally de-indented standalone YAML.
59    ///
60    /// # Errors
61    ///
62    /// Returns an error when the stored root node is no longer valid.
63    pub fn to_yaml(&self) -> Result<String, FragmentError> {
64        self.doc
65            .extract_node(self.root)
66            .map_err(FragmentError::from)
67    }
68
69    pub(crate) fn contains_anchor(&self) -> bool {
70        self.subtree_nodes()
71            .any(|node| self.doc.anchor(node).is_some())
72    }
73
74    pub(crate) fn from_document_node(doc: &YamlDoc, root: NodeId) -> Result<Self, FragmentError> {
75        doc.node(root)
76            .ok_or_else(|| FragmentError::new("fragment source node is missing"))?;
77        Ok(Self {
78            doc: doc.clone(),
79            root,
80        })
81    }
82
83    pub(crate) fn prepared(&self, target: &YamlDoc) -> Result<Self, FragmentError> {
84        self.prepared_with_anchor_exemption(target, None)
85    }
86
87    pub(crate) fn prepared_for_replacement(
88        &self,
89        target: &YamlDoc,
90        replaced: NodeId,
91    ) -> Result<Self, FragmentError> {
92        self.prepared_with_anchor_exemption(target, target.anchor(replaced))
93    }
94
95    fn prepared_with_anchor_exemption(
96        &self,
97        target: &YamlDoc,
98        exempt_anchor: Option<&str>,
99    ) -> Result<Self, FragmentError> {
100        let mut used = target.anchor_names();
101        if let Some(anchor) = exempt_anchor {
102            used.remove(anchor);
103        }
104        let mut renamed = BTreeMap::new();
105        for node in self.subtree_nodes() {
106            let Some(name) = self.doc.anchor(node) else {
107                continue;
108            };
109            if used.insert(name.to_owned()) {
110                continue;
111            }
112            let mut suffix = 1_u64;
113            let replacement = loop {
114                let candidate = format!("{name}_{suffix}");
115                if used.insert(candidate.clone()) {
116                    break candidate;
117                }
118                suffix = suffix.saturating_add(1);
119            };
120            renamed.insert(name.to_owned(), replacement);
121        }
122        if renamed.is_empty() {
123            return Ok(self.clone());
124        }
125
126        let mut doc = self.doc.clone();
127        let nodes = self.subtree_nodes().collect::<Vec<_>>();
128        for node in nodes {
129            let Some(properties) = doc.semantic_properties(node) else {
130                continue;
131            };
132            if let Some(span) = properties.anchor {
133                let old = doc.source.slice(span);
134                if let Some(new) = renamed.get(old) {
135                    doc.queue_edit(span, new.clone())
136                        .map_err(FragmentError::from)?;
137                }
138            }
139            if let Some(span) = properties.alias {
140                let old = doc.source.slice(span);
141                if let Some(new) = renamed.get(old) {
142                    doc.queue_edit(span, new.clone())
143                        .map_err(FragmentError::from)?;
144                }
145            }
146        }
147        doc.commit_edits().map_err(FragmentError::from)?;
148        let root = doc
149            .document_root(0)
150            .map_err(FragmentError::from)?
151            .ok_or_else(|| FragmentError::new("prepared fragment lost its root node"))?;
152        Ok(Self { doc, root })
153    }
154
155    pub(crate) fn render_flow(&self, target: &YamlDoc) -> Result<String, FragmentError> {
156        let prepared = self.prepared(target)?;
157        prepared.render_node_flow(prepared.root, 0)
158    }
159
160    pub(crate) fn render_flow_for_replacement(
161        &self,
162        target: &YamlDoc,
163        replaced: NodeId,
164    ) -> Result<String, FragmentError> {
165        let prepared = self.prepared_for_replacement(target, replaced)?;
166        prepared.render_node_flow(prepared.root, 0)
167    }
168
169    fn render_node_flow(&self, node: NodeId, depth: usize) -> Result<String, FragmentError> {
170        enum RenderAction {
171            Node(NodeId, usize),
172            Text(&'static str),
173        }
174
175        let mut output = String::new();
176        let mut pending = vec![RenderAction::Node(node, depth)];
177        while let Some(action) = pending.pop() {
178            let RenderAction::Node(node, depth) = action else {
179                let RenderAction::Text(text) = action else {
180                    unreachable!();
181                };
182                output.push_str(text);
183                continue;
184            };
185            if depth > 1024 {
186                return Err(FragmentError::new(
187                    "fragment rendering recursion limit exceeded",
188                ));
189            }
190            match self.doc.semantic_kind(node) {
191                Some(SemanticKind::Alias) => {
192                    output.push('*');
193                    output.push_str(self.doc.alias_name(node).unwrap_or_default());
194                }
195                Some(SemanticKind::Scalar { style }) => {
196                    let prefix = self.property_prefix(node);
197                    if matches!(style, YamlScalarStyle::Literal | YamlScalarStyle::Folded)
198                        || self
199                            .doc
200                            .node(node)
201                            .is_some_and(|node| node.span().is_empty())
202                    {
203                        let value = self.doc.scalar_value(node).map_err(FragmentError::from)?;
204                        output.push_str(&prefix);
205                        output.push_str(&quote_string(&value));
206                    } else {
207                        let source = self.doc.extract_node(node).map_err(FragmentError::from)?;
208                        let source = strip_inline_comment(&source).trim_end();
209                        if source.contains(['\n', '\r'])
210                            || style == YamlScalarStyle::Plain
211                                && source.contains(['[', ']', '{', '}', ','])
212                        {
213                            let value = self.doc.scalar_value(node).map_err(FragmentError::from)?;
214                            output.push_str(&prefix);
215                            output.push_str(&quote_string(&value));
216                        } else {
217                            output.push_str(source);
218                        }
219                    }
220                }
221                Some(SemanticKind::Sequence { .. }) => {
222                    output.push_str(&self.property_prefix(node));
223                    output.push('[');
224                    pending.push(RenderAction::Text("]"));
225                    let items = self.doc.sequence_items(node).collect::<Vec<_>>();
226                    for (index, item) in items.into_iter().enumerate().rev() {
227                        pending.push(RenderAction::Node(item, depth + 1));
228                        if index > 0 {
229                            pending.push(RenderAction::Text(", "));
230                        }
231                    }
232                }
233                Some(SemanticKind::Mapping { .. }) => {
234                    output.push_str(&self.property_prefix(node));
235                    output.push('{');
236                    pending.push(RenderAction::Text("}"));
237                    let entries = self.doc.mapping_entries(node).collect::<Vec<_>>();
238                    for (index, (key, value)) in entries.into_iter().enumerate().rev() {
239                        pending.push(RenderAction::Node(value, depth + 1));
240                        pending.push(RenderAction::Text(": "));
241                        pending.push(RenderAction::Node(key, depth + 1));
242                        if index > 0 {
243                            pending.push(RenderAction::Text(", "));
244                        }
245                    }
246                }
247                Some(SemanticKind::Document) | None => {
248                    return Err(FragmentError::new("cannot render unknown YAML node"));
249                }
250            }
251        }
252        Ok(output)
253    }
254
255    fn property_prefix(&self, node: NodeId) -> String {
256        let mut prefix = String::new();
257        if let Some(tag) = self.doc.raw_tag(node) {
258            prefix.push_str(tag);
259            prefix.push(' ');
260        }
261        if let Some(anchor) = self.doc.anchor(node) {
262            prefix.push('&');
263            prefix.push_str(anchor);
264            prefix.push(' ');
265        }
266        prefix
267    }
268
269    fn subtree_nodes(&self) -> impl Iterator<Item = NodeId> + '_ {
270        let span = self.doc.node(self.root).map(super::syntax::Node::span);
271        self.doc
272            .nodes
273            .iter()
274            .enumerate()
275            .map(|(index, _)| NodeId::from_usize(index))
276            .filter(move |node| {
277                let Some(root_span) = span else {
278                    return false;
279                };
280                self.doc.node(*node).is_some_and(|node| {
281                    node.span().start >= root_span.start && node.span().end <= root_span.end
282                })
283            })
284    }
285
286    fn validate_alias_scope(&self) -> Result<(), FragmentError> {
287        let root_span = self
288            .doc
289            .node(self.root)
290            .map(super::syntax::Node::span)
291            .ok_or_else(|| FragmentError::new("fragment root node is missing"))?;
292        for node in self.subtree_nodes() {
293            if !matches!(self.doc.semantic_kind(node), Some(SemanticKind::Alias)) {
294                continue;
295            }
296            let target = self.doc.resolve_alias(node).ok_or_else(|| {
297                FragmentError::new(format!(
298                    "unresolved value alias `*{}`",
299                    self.doc.alias_name(node).unwrap_or_default()
300                ))
301            })?;
302            let target_span = self
303                .doc
304                .node(target)
305                .map(super::syntax::Node::span)
306                .ok_or_else(|| FragmentError::new("alias target is missing"))?;
307            if target_span.start < root_span.start || target_span.end > root_span.end {
308                return Err(FragmentError::new(
309                    "a value alias cannot reference a node outside the value root",
310                ));
311            }
312        }
313        Ok(())
314    }
315}
316
317impl YamlDoc {
318    /// Extracts one semantic node as valid standalone YAML where possible.
319    ///
320    /// # Errors
321    ///
322    /// Returns an error when `node` does not identify a node in this document.
323    pub fn extract_node(&self, node: NodeId) -> Result<String, YamlError> {
324        let node = self.expect_node(node)?;
325        let source = self.source.slice(node.span);
326        let line_start = self.source.as_str()[..node.span.start as usize]
327            .rfind(['\n', '\r'])
328            .map_or(0, |index| index + 1);
329        let base_indent = node.span.start as usize - line_start;
330        Ok(deindent_continuation_lines(source, base_indent))
331    }
332
333    pub(crate) fn anchor_names(&self) -> BTreeSet<String> {
334        self.nodes
335            .iter()
336            .enumerate()
337            .filter_map(|(index, _)| self.anchor(NodeId::from_usize(index)).map(str::to_owned))
338            .collect()
339    }
340}
341
342fn deindent_continuation_lines(source: &str, indent: usize) -> String {
343    if indent == 0 || !source.contains(['\n', '\r']) {
344        return source.to_owned();
345    }
346    let bytes = source.as_bytes();
347    let mut output = String::with_capacity(source.len());
348    let mut position = 0;
349    let mut first = true;
350    while position < bytes.len() {
351        if !first {
352            let mut removed = 0;
353            while removed < indent && bytes.get(position) == Some(&b' ') {
354                position += 1;
355                removed += 1;
356            }
357        }
358        first = false;
359        let line_end = source[position..]
360            .find(['\n', '\r'])
361            .map_or(source.len(), |offset| position + offset);
362        output.push_str(&source[position..line_end]);
363        position = line_end;
364        if bytes.get(position) == Some(&b'\r') {
365            output.push('\r');
366            position += 1;
367            if bytes.get(position) == Some(&b'\n') {
368                output.push('\n');
369                position += 1;
370            }
371        } else if bytes.get(position) == Some(&b'\n') {
372            output.push('\n');
373            position += 1;
374        }
375    }
376    output
377}
378
379pub(crate) fn indent_text(source: &str, indent: usize) -> String {
380    if indent == 0 || source.is_empty() {
381        return source.to_owned();
382    }
383    let prefix = " ".repeat(indent);
384    let mut output = String::with_capacity(source.len() + prefix.len());
385    let mut at_line_start = true;
386    for character in source.chars() {
387        if at_line_start && !matches!(character, '\r' | '\n') {
388            output.push_str(&prefix);
389            at_line_start = false;
390        }
391        output.push(character);
392        if character == '\n' || character == '\r' {
393            at_line_start = true;
394        }
395    }
396    output
397}
398
399pub(crate) fn quote_string(value: &str) -> String {
400    let mut output = String::from("\"");
401    for character in value.chars() {
402        match character {
403            '"' => output.push_str("\\\""),
404            '\\' => output.push_str("\\\\"),
405            '\n' => output.push_str("\\n"),
406            '\r' => output.push_str("\\r"),
407            '\t' => output.push_str("\\t"),
408            character if character.is_control() => {
409                write!(output, "\\u{:04X}", u32::from(character))
410                    .expect("writing to a String cannot fail");
411            }
412            character => output.push(character),
413        }
414    }
415    output.push('"');
416    output
417}
418
419/// Failure to parse, validate, or render a YAML fragment.
420#[derive(Debug, Clone, PartialEq, Eq)]
421pub struct FragmentError {
422    message: String,
423}
424
425impl FragmentError {
426    pub(crate) fn new(message: impl Into<String>) -> Self {
427        Self {
428            message: message.into(),
429        }
430    }
431}
432
433impl fmt::Display for FragmentError {
434    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
435        formatter.write_str(&self.message)
436    }
437}
438
439impl std::error::Error for FragmentError {}
440
441impl From<YamlError> for FragmentError {
442    fn from(error: YamlError) -> Self {
443        Self::new(error.to_string())
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450
451    fn nested_block_sequence(depth: usize) -> String {
452        let mut yaml = String::new();
453        for level in 0..depth {
454            yaml.push_str(&"  ".repeat(level));
455            yaml.push_str("-\n");
456        }
457        yaml.push_str(&"  ".repeat(depth));
458        yaml.push_str("value\n");
459        yaml
460    }
461
462    #[test]
463    fn fragment_requires_one_nonempty_document() {
464        assert!(YamlFragment::parse("").is_err());
465        assert!(YamlFragment::parse("--- a\n--- b\n").is_err());
466        assert!(YamlFragment::parse("[a, b]").is_ok());
467    }
468
469    #[test]
470    fn extraction_deindents_nested_block_nodes() {
471        let doc = YamlDoc::parse("outer:\n  one: 1\n  two:\n    - a\n    - b\n").unwrap();
472        let node = doc
473            .get_mapping_value(doc.document_root_mapping(0).unwrap(), "outer")
474            .unwrap()
475            .unwrap();
476        assert_eq!(
477            doc.extract_node(node).unwrap(),
478            "one: 1\ntwo:\n  - a\n  - b"
479        );
480    }
481
482    #[test]
483    fn extraction_deindents_block_mapping_after_sequence_marker() {
484        let doc = YamlDoc::parse(
485            "services:\n  - name: api\n    port: 8080 # public endpoint\n    enabled: TRUE\n",
486        )
487        .unwrap();
488        let node = doc
489            .resolve_pointer(0, &crate::JsonPointer::parse("/services/0").unwrap())
490            .unwrap();
491
492        assert_eq!(
493            doc.extract_node(node).unwrap(),
494            "name: api\nport: 8080 # public endpoint\nenabled: TRUE"
495        );
496    }
497
498    #[test]
499    fn rejects_unresolved_value_aliases() {
500        assert!(YamlFragment::parse("*outside").is_err());
501    }
502
503    #[test]
504    fn flow_rendering_normalizes_block_collections_only() {
505        let fragment = YamlFragment::parse("one:\n  - a\n  - b\n").unwrap();
506        let target = YamlDoc::parse("target: []\n").unwrap();
507        assert_eq!(fragment.render_flow(&target).unwrap(), "{one: [a, b]}");
508    }
509
510    #[test]
511    fn flow_rendering_preserves_the_existing_depth_limit() {
512        std::thread::Builder::new()
513            .stack_size(32 * 1024 * 1024)
514            .spawn(|| {
515                let target = YamlDoc::parse("target: []\n").unwrap();
516                let accepted = YamlFragment::parse(&nested_block_sequence(1024)).unwrap();
517                assert!(accepted.render_flow(&target).is_ok());
518
519                let rejected = YamlFragment::parse(&nested_block_sequence(1025)).unwrap();
520                assert!(
521                    rejected
522                        .render_flow(&target)
523                        .unwrap_err()
524                        .to_string()
525                        .contains("recursion limit")
526                );
527            })
528            .unwrap()
529            .join()
530            .unwrap();
531    }
532}