Skip to main content

asdf_yaml/
parse.rs

1//! Building a [`Document`] from `saphyr-parser`'s event stream.
2//!
3//! We consume events rather than `saphyr`'s own `Yaml` tree because the tree
4//! type flattens the anchor graph -- its `Yaml::Alias` is documented as "not
5//! fully supported" -- while ASDF needs aliases to stay distinct from their
6//! targets. The event stream carries anchor ids and tags, which is everything
7//! required to rebuild the graph faithfully.
8
9use std::collections::HashMap;
10
11use saphyr_parser::{Event, Parser, ScanError, Span as SapSpan, SpannedEventReceiver};
12
13use crate::document::{Document, YamlVersion};
14use crate::node::{CollectionStyle, Entry, Node, NodeData, NodeId, ScalarStyle, Span};
15use crate::tag::{Tag, TagHandle};
16
17/// An error encountered while parsing a YAML document.
18#[derive(Debug, thiserror::Error)]
19pub enum ParseError {
20    /// The underlying YAML scanner rejected the input.
21    #[error("YAML parse error: {0}")]
22    Scan(#[from] ScanError),
23    /// The document was empty where a document was required.
24    #[error("no YAML document found")]
25    Empty,
26    /// The event stream was not well-formed. Reaching this indicates a bug
27    /// rather than bad input, since the scanner validates structure first.
28    #[error("malformed YAML event stream: {0}")]
29    Malformed(&'static str),
30}
31
32/// What the builder is part-way through assembling.
33enum Frame {
34    Sequence { items: Vec<NodeId>, node: NodeId, start: usize },
35    Mapping { entries: Vec<Entry>, pending_key: Option<NodeId>, node: NodeId, start: usize },
36}
37
38#[derive(Default)]
39struct Builder {
40    doc: Document,
41    stack: Vec<Frame>,
42    /// Maps a parser anchor id to the node that defined it.
43    anchors: HashMap<usize, NodeId>,
44    /// Anchor ids seen, in first-seen order, so we can name them on emit.
45    anchor_order: Vec<usize>,
46    root: Option<NodeId>,
47    done: bool,
48    error: Option<ParseError>,
49}
50
51fn convert_style(style: saphyr_parser::ScalarStyle) -> ScalarStyle {
52    use saphyr_parser::ScalarStyle as S;
53    match style {
54        S::Plain => ScalarStyle::Plain,
55        S::SingleQuoted => ScalarStyle::SingleQuoted,
56        S::DoubleQuoted => ScalarStyle::DoubleQuoted,
57        S::Literal => ScalarStyle::Literal,
58        S::Folded => ScalarStyle::Folded,
59    }
60}
61
62fn convert_span(span: SapSpan) -> Span {
63    Span { start: span.start.index(), end: span.end.index() }
64}
65
66impl Builder {
67    /// Record a completed node into whatever container is currently open.
68    fn place(&mut self, id: NodeId) {
69        match self.stack.last_mut() {
70            None => {
71                if self.root.is_none() {
72                    self.root = Some(id);
73                }
74            }
75            Some(Frame::Sequence { items, .. }) => items.push(id),
76            Some(Frame::Mapping { entries, pending_key, .. }) => match pending_key.take() {
77                None => *pending_key = Some(id),
78                Some(key) => entries.push(Entry { key, value: id }),
79            },
80        }
81    }
82
83    /// Register an anchor definition, if the event carried one.
84    ///
85    /// Anchor id 0 is the parser's "no anchor" sentinel.
86    fn note_anchor(&mut self, anchor_id: usize, node: NodeId) {
87        if anchor_id == 0 {
88            return;
89        }
90        if self.anchors.insert(anchor_id, node).is_none() {
91            self.anchor_order.push(anchor_id);
92        }
93        // Name the anchor positionally; the source name is not surfaced by the
94        // parser, and ASDF assigns no meaning to anchor names.
95        let position = self.anchor_order.iter().position(|a| *a == anchor_id).unwrap_or(0);
96        self.doc.node_mut(node).anchor = Some(format!("anc{}", position));
97    }
98
99    fn open(&mut self, frame: Frame) {
100        self.stack.push(frame);
101    }
102
103    fn close(&mut self) -> Result<(), ParseError> {
104        let frame = self
105            .stack
106            .pop()
107            .ok_or(ParseError::Malformed("collection end without a matching start"))?;
108
109        let (node, data, start) = match frame {
110            Frame::Sequence { items, node, start } => {
111                (node, NodeData::Sequence { items, style: CollectionStyle::Auto }, start)
112            }
113            Frame::Mapping { entries, pending_key, node, start } => {
114                if pending_key.is_some() {
115                    return Err(ParseError::Malformed("mapping ended with a dangling key"));
116                }
117                (node, NodeData::Mapping { entries, style: CollectionStyle::Auto }, start)
118            }
119        };
120
121        let n = self.doc.node_mut(node);
122        n.data = data;
123        if let Some(span) = n.span.as_mut() {
124            span.start = start;
125        }
126        self.place(node);
127        Ok(())
128    }
129
130    fn handle(&mut self, ev: Event<'_>, span: SapSpan) -> Result<(), ParseError> {
131        // Only the first document in the stream is retained; ASDF permits
132        // exactly one, and a block index is parsed separately.
133        if self.done {
134            return Ok(());
135        }
136
137        match ev {
138            Event::StreamStart | Event::StreamEnd | Event::Nothing => {}
139            Event::DocumentStart(_) => {}
140            Event::DocumentEnd => {
141                if self.root.is_some() {
142                    self.done = true;
143                }
144            }
145
146            Event::Scalar(value, style, anchor_id, tag) => {
147                let mut node = Node::scalar_styled(value.into_owned(), convert_style(style));
148                node.tag = tag.map(|t| Tag::new(t.handle.clone(), t.suffix.clone()));
149                node.span = Some(convert_span(span));
150                let id = self.doc.add(node);
151                self.note_anchor(anchor_id, id);
152                self.place(id);
153            }
154
155            Event::Alias(anchor_id) => {
156                let target = self.anchors.get(&anchor_id).copied().ok_or(ParseError::Malformed(
157                    "alias refers to an anchor that was never defined",
158                ))?;
159                let mut node = Node::new(NodeData::Alias(target));
160                node.span = Some(convert_span(span));
161                let id = self.doc.add(node);
162                self.place(id);
163            }
164
165            Event::SequenceStart(anchor_id, tag) => {
166                // The node is allocated up front so that an alias appearing
167                // inside it can already refer to it.
168                let mut node = Node::sequence();
169                node.tag = tag.map(|t| Tag::new(t.handle.clone(), t.suffix.clone()));
170                node.span = Some(convert_span(span));
171                let id = self.doc.add(node);
172                self.note_anchor(anchor_id, id);
173                self.open(Frame::Sequence {
174                    items: Vec::new(),
175                    node: id,
176                    start: span.start.index(),
177                });
178            }
179
180            Event::MappingStart(anchor_id, tag) => {
181                let mut node = Node::mapping();
182                node.tag = tag.map(|t| Tag::new(t.handle.clone(), t.suffix.clone()));
183                node.span = Some(convert_span(span));
184                let id = self.doc.add(node);
185                self.note_anchor(anchor_id, id);
186                self.open(Frame::Mapping {
187                    entries: Vec::new(),
188                    pending_key: None,
189                    node: id,
190                    start: span.start.index(),
191                });
192            }
193
194            Event::SequenceEnd | Event::MappingEnd => self.close()?,
195        }
196        Ok(())
197    }
198}
199
200impl<'i> SpannedEventReceiver<'i> for Builder {
201    fn on_event(&mut self, ev: Event<'i>, span: SapSpan) {
202        if self.error.is_some() {
203            return;
204        }
205        if let Err(e) = self.handle(ev, span) {
206            self.error = Some(e);
207        }
208    }
209}
210
211/// Read the `%YAML` and `%TAG` directives from the head of a document.
212///
213/// `saphyr-parser` consumes directives internally and surfaces no event for
214/// them -- it hands back tags with their handle already expanded -- so they
215/// are scanned from the source instead. Without this the emitter could not
216/// reproduce the `%TAG ! tag:stsci.edu:asdf/` line that lets an ASDF tree
217/// write `!core/ndarray-1.1.0` rather than the full URI.
218fn scan_directives(input: &str, doc: &mut Document) {
219    for line in input.lines() {
220        let line = line.trim_end();
221        if line.starts_with("---") || line.starts_with("...") {
222            break;
223        }
224        if let Some(rest) = line.strip_prefix("%YAML ") {
225            let mut parts = rest.trim().split('.');
226            if let (Some(major), Some(minor)) = (parts.next(), parts.next())
227                && let (Ok(major), Ok(minor)) = (major.parse(), minor.parse())
228            {
229                doc.version = Some(YamlVersion { major, minor });
230            }
231        } else if let Some(rest) = line.strip_prefix("%TAG ") {
232            let mut parts = rest.split_whitespace();
233            if let (Some(handle), Some(prefix)) = (parts.next(), parts.next()) {
234                doc.tag_handles
235                    .push(TagHandle { handle: handle.to_string(), prefix: prefix.to_string() });
236            }
237        }
238    }
239}
240
241/// Parse a YAML document into a [`Document`].
242///
243/// Only the first document in the stream is returned. Anchors and aliases are
244/// preserved as distinct nodes rather than being expanded, and the `%YAML`
245/// and `%TAG` directives are recorded so the document can be re-emitted with
246/// them intact.
247pub fn parse_document(input: &str) -> Result<Document, ParseError> {
248    let mut builder = Builder::default();
249    let parse_result = Parser::new_from_str(input).load(&mut builder, true);
250
251    if let Some(err) = builder.error {
252        return Err(err);
253    }
254    parse_result?;
255
256    let root = builder.root.ok_or(ParseError::Empty)?;
257    let mut doc = builder.doc;
258    doc.set_root(root);
259    scan_directives(input, &mut doc);
260    Ok(doc)
261}
262
263/// A YAML parse event, as the low-level event API surfaces it.
264///
265/// This is deliberately flat rather than a tree: it exists for the streaming
266/// API, which reports what the scanner saw in the order it saw it.
267#[derive(Clone, Copy, PartialEq, Eq, Debug)]
268pub enum YamlEventKind {
269    StreamStart,
270    StreamEnd,
271    DocumentStart,
272    DocumentEnd,
273    MappingStart,
274    MappingEnd,
275    SequenceStart,
276    SequenceEnd,
277    Scalar,
278    Alias,
279}
280
281/// One event from [`scan_events`].
282#[derive(Clone, PartialEq, Eq, Debug)]
283pub struct YamlEvent {
284    pub kind: YamlEventKind,
285    /// The node's tag in full URI form, when it carried one.
286    pub tag: Option<String>,
287    /// The scalar's text, for [`YamlEventKind::Scalar`].
288    pub value: Option<String>,
289}
290
291/// Collects the raw event stream without building a tree.
292#[derive(Default)]
293struct EventCollector {
294    events: Vec<YamlEvent>,
295}
296
297impl EventCollector {
298    fn push(&mut self, kind: YamlEventKind, tag: Option<String>, value: Option<String>) {
299        self.events.push(YamlEvent { kind, tag, value });
300    }
301}
302
303impl<'i> SpannedEventReceiver<'i> for EventCollector {
304    fn on_event(&mut self, ev: Event<'i>, _span: SapSpan) {
305        let tag_of = |tag: Option<alloc::borrow::Cow<'_, saphyr_parser::Tag>>| {
306            tag.map(|t| Tag::new(t.handle.clone(), t.suffix.clone()).full())
307        };
308        match ev {
309            Event::Nothing => {}
310            Event::StreamStart => self.push(YamlEventKind::StreamStart, None, None),
311            Event::StreamEnd => self.push(YamlEventKind::StreamEnd, None, None),
312            Event::DocumentStart(_) => self.push(YamlEventKind::DocumentStart, None, None),
313            Event::DocumentEnd => self.push(YamlEventKind::DocumentEnd, None, None),
314            Event::MappingStart(_, tag) => {
315                self.push(YamlEventKind::MappingStart, tag_of(tag), None);
316            }
317            Event::MappingEnd => self.push(YamlEventKind::MappingEnd, None, None),
318            Event::SequenceStart(_, tag) => {
319                self.push(YamlEventKind::SequenceStart, tag_of(tag), None);
320            }
321            Event::SequenceEnd => self.push(YamlEventKind::SequenceEnd, None, None),
322            Event::Scalar(value, _, _, tag) => {
323                self.push(YamlEventKind::Scalar, tag_of(tag), Some(value.into_owned()));
324            }
325            Event::Alias(_) => self.push(YamlEventKind::Alias, None, None),
326        }
327    }
328}
329
330/// The raw YAML event stream for a document, without building a tree.
331///
332/// Used by the low-level event API, which reports YAML sub-events as the
333/// scanner produces them.
334pub fn scan_events(input: &str) -> Result<Vec<YamlEvent>, ParseError> {
335    let mut collector = EventCollector::default();
336    Parser::new_from_str(input).load(&mut collector, true)?;
337    Ok(collector.events)
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn raw_events_carry_tags_and_scalars() {
346        let events = scan_events(
347            "%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\nk: v\nl: [1]\n",
348        )
349        .unwrap();
350
351        let kinds: Vec<YamlEventKind> = events.iter().map(|e| e.kind).collect();
352        assert_eq!(
353            kinds,
354            vec![
355                YamlEventKind::StreamStart,
356                YamlEventKind::DocumentStart,
357                YamlEventKind::MappingStart,
358                YamlEventKind::Scalar,
359                YamlEventKind::Scalar,
360                YamlEventKind::Scalar,
361                YamlEventKind::SequenceStart,
362                YamlEventKind::Scalar,
363                YamlEventKind::SequenceEnd,
364                YamlEventKind::MappingEnd,
365                YamlEventKind::DocumentEnd,
366                YamlEventKind::StreamEnd,
367            ]
368        );
369
370        // The root's tag is reported expanded through the `%TAG` directive.
371        assert_eq!(events[2].tag.as_deref(), Some("tag:stsci.edu:asdf/core/asdf-1.1.0"));
372        assert_eq!(events[3].value.as_deref(), Some("k"));
373        assert_eq!(events[4].value.as_deref(), Some("v"));
374        assert!(events[3].tag.is_none());
375    }
376
377    #[test]
378    fn raw_events_report_aliases_without_expanding_them() {
379        let events = scan_events("a: &x 1\nb: *x\n").unwrap();
380        assert_eq!(events.iter().filter(|e| e.kind == YamlEventKind::Alias).count(), 1);
381    }
382
383    use crate::tag::ASDF_STANDARD_TAG_PREFIX;
384
385    #[test]
386    fn parses_a_minimal_asdf_tree() {
387        let doc = parse_document(
388            "%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\nfoo: 42\n...\n",
389        )
390        .unwrap();
391
392        let root = doc.root().unwrap();
393        assert!(doc.node(root).is_mapping());
394
395        let tag = doc.tag_of(root).unwrap();
396        assert_eq!(tag.handle(), ASDF_STANDARD_TAG_PREFIX);
397        assert_eq!(tag.suffix(), "core/asdf-1.1.0");
398
399        let foo = doc.mapping_get(root, "foo").unwrap();
400        assert_eq!(doc.node(foo).as_str(), Some("42"));
401    }
402
403    #[test]
404    fn preserves_scalar_style() {
405        let doc = parse_document("a: yes\nb: \"yes\"\nc: 'yes'\n").unwrap();
406        let root = doc.root().unwrap();
407
408        let a = doc.node(doc.mapping_get(root, "a").unwrap());
409        let b = doc.node(doc.mapping_get(root, "b").unwrap());
410        let c = doc.node(doc.mapping_get(root, "c").unwrap());
411
412        assert_eq!(a.scalar_style(), Some(ScalarStyle::Plain));
413        assert_eq!(b.scalar_style(), Some(ScalarStyle::DoubleQuoted));
414        assert_eq!(c.scalar_style(), Some(ScalarStyle::SingleQuoted));
415        // all three carry the same text; only the style distinguishes them
416        assert_eq!(a.as_str(), Some("yes"));
417        assert_eq!(b.as_str(), Some("yes"));
418    }
419
420    #[test]
421    fn aliases_share_their_target() {
422        let doc = parse_document("shared: &a {x: 1}\nother: *a\n").unwrap();
423        let root = doc.root().unwrap();
424
425        let shared = doc.mapping_get(root, "shared").unwrap();
426        let other = doc.mapping_get(root, "other").unwrap();
427
428        assert!(!doc.node(shared).is_alias());
429        assert!(doc.node(other).is_alias(), "alias must stay distinct from its target");
430        assert_eq!(doc.resolve(other), shared, "alias must resolve to the same node");
431        assert!(doc.node(shared).anchor.is_some());
432
433        // reading through the alias sees the shared value
434        let x = doc.mapping_get(other, "x").unwrap();
435        assert_eq!(doc.node(x).as_str(), Some("1"));
436    }
437
438    #[test]
439    fn alias_to_a_sequence_element() {
440        let doc = parse_document("- &v 7\n- *v\n").unwrap();
441        let root = doc.root().unwrap();
442        let items = doc.sequence_items(root).unwrap().to_vec();
443        assert_eq!(items.len(), 2);
444        assert!(doc.node(items[1]).is_alias());
445        assert_eq!(doc.resolved(items[1]).as_str(), Some("7"));
446    }
447
448    #[test]
449    fn nested_tags_are_kept() {
450        let doc = parse_document(
451            "%YAML 1.1\n%TAG ! tag:stsci.edu:asdf/\n--- !core/asdf-1.1.0\n\
452             data: !core/ndarray-1.1.0\n  source: 0\n  shape: [8]\n...\n",
453        )
454        .unwrap();
455        let root = doc.root().unwrap();
456        let data = doc.mapping_get(root, "data").unwrap();
457        assert_eq!(doc.tag_of(data).unwrap().full(), "tag:stsci.edu:asdf/core/ndarray-1.1.0");
458        let shape = doc.mapping_get(data, "shape").unwrap();
459        assert_eq!(doc.container_len(shape), Some(1));
460    }
461
462    #[test]
463    fn empty_input_is_an_error() {
464        assert!(matches!(parse_document(""), Err(ParseError::Empty)));
465    }
466
467    #[test]
468    fn malformed_yaml_is_an_error() {
469        assert!(parse_document("a: [1, 2\nb: 3\n").is_err());
470    }
471
472    #[test]
473    fn spans_locate_nodes_in_the_source() {
474        let src = "foo: 42\n";
475        let doc = parse_document(src).unwrap();
476        let root = doc.root().unwrap();
477        let foo = doc.mapping_get(root, "foo").unwrap();
478        let span = doc.node(foo).span.unwrap();
479        assert_eq!(&src[span.start..span.end], "42");
480    }
481}