granit-parser 0.0.4

A YAML parser with comment and style support, written in pure Rust
Documentation
use granit_parser::{
    input::SkipTabs, BorrowedInput, Event, Input, Parser, ScanError, Scanner, Span, StrInput, Token,
};

struct CommentEnabledStrInput<'input> {
    inner: StrInput<'input>,
}

impl<'input> CommentEnabledStrInput<'input> {
    #[must_use]
    fn new(source: &'input str) -> Self {
        Self {
            inner: StrInput::new(source),
        }
    }
}

impl Input for CommentEnabledStrInput<'_> {
    fn lookahead(&mut self, count: usize) {
        self.inner.lookahead(count);
    }

    fn buflen(&self) -> usize {
        self.inner.buflen()
    }

    fn bufmaxlen(&self) -> usize {
        self.inner.bufmaxlen()
    }

    fn raw_read_ch(&mut self) -> char {
        self.inner.raw_read_ch()
    }

    fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
        self.inner.raw_read_non_breakz_ch()
    }

    fn skip(&mut self) {
        self.inner.skip();
    }

    fn skip_n(&mut self, count: usize) {
        self.inner.skip_n(count);
    }

    fn peek(&self) -> char {
        self.inner.peek()
    }

    fn peek_nth(&self, n: usize) -> char {
        self.inner.peek_nth(n)
    }

    fn byte_offset(&self) -> Option<usize> {
        self.inner.byte_offset()
    }

    fn slice_bytes(&self, start: usize, end: usize) -> Option<&str> {
        self.inner.slice_bytes(start, end)
    }

    fn may_contain_comments(&self) -> bool {
        true
    }

    fn skip_ws_to_eol(&mut self, skip_tabs: SkipTabs) -> (usize, Result<SkipTabs, &'static str>) {
        self.inner.skip_ws_to_eol(skip_tabs)
    }

    fn skip_ws_to_eol_blanks(&mut self, skip_tabs: SkipTabs) -> (usize, SkipTabs) {
        self.inner.skip_ws_to_eol_blanks(skip_tabs)
    }
}

impl<'input> BorrowedInput<'input> for CommentEnabledStrInput<'input> {
    fn slice_borrowed(&self, start: usize, end: usize) -> Option<&'input str> {
        self.inner.slice_borrowed(start, end)
    }
}

#[derive(Debug, PartialEq, Eq)]
struct ParseTrace<'input> {
    events: Vec<(Event<'input>, Span)>,
    error: Option<ScanError>,
}

fn scanner_tokens_fast_path(source: &str) -> Vec<Token<'_>> {
    Scanner::new(StrInput::new(source)).collect()
}

fn scanner_tokens_comment_enabled(source: &str) -> Vec<Token<'_>> {
    Scanner::new(CommentEnabledStrInput::new(source)).collect()
}

fn parser_trace_fast_path(source: &str) -> ParseTrace<'_> {
    let mut parser = Parser::new_from_str(source);
    parser_trace(&mut parser)
}

fn parser_trace_comment_enabled(source: &str) -> ParseTrace<'_> {
    let mut parser = Parser::new(CommentEnabledStrInput::new(source));
    parser_trace(&mut parser)
}

fn parser_trace<'input, T>(parser: &mut Parser<'input, T>) -> ParseTrace<'input>
where
    T: BorrowedInput<'input>,
{
    let mut events = Vec::new();
    let mut error = None;

    while let Some(next) = parser.next_event() {
        match next {
            Ok(event) => events.push(event),
            Err(err) => {
                error = Some(err);
                break;
            }
        }
    }

    ParseTrace { events, error }
}

fn assert_no_hash(source: &str, name: &str) {
    assert!(
        !source.contains('#'),
        "{name} fixture must exercise the no-comment fast path"
    );
    assert!(
        !StrInput::new(source).may_contain_comments(),
        "{name} should make StrInput disable comment probing"
    );
    assert!(
        CommentEnabledStrInput::new(source).may_contain_comments(),
        "{name} control input should keep comment probing enabled"
    );
}

#[test]
fn no_comment_fast_path_matches_comment_enabled_tokens_and_events() {
    for (name, source) in [
        ("block mapping", "a: b\n"),
        ("block sequence value", "a:\n  - b\n"),
        ("flow sequence", "[a, b]\n"),
        ("flow mapping", "{a: b}\n"),
        ("explicit document", "---\na: b\n...\n"),
    ] {
        assert_no_hash(source, name);

        assert_eq!(
            scanner_tokens_fast_path(source),
            scanner_tokens_comment_enabled(source),
            "{name} scanner token output differed"
        );
        assert_eq!(
            parser_trace_fast_path(source),
            parser_trace_comment_enabled(source),
            "{name} parser event output differed"
        );
    }
}

#[test]
fn invalid_no_comment_fast_path_matches_comment_enabled_event_prefixes() {
    for (name, source) in [
        ("unclosed flow sequence", "a: [1, 2\n"),
        ("extra flow sequence end", "key: [1, 2]]\n"),
        ("tab after value indicator", "a:\tb\n"),
        (
            "directive after implicit document",
            "a: b\n%YAML 1.2\n---\nc: d\n",
        ),
        ("reserved indicator", "@\n"),
    ] {
        assert_no_hash(source, name);

        let fast_path = parser_trace_fast_path(source);
        let comment_enabled = parser_trace_comment_enabled(source);

        assert!(fast_path.error.is_some(), "{name} should fail");
        assert_eq!(
            fast_path, comment_enabled,
            "{name} parser event prefix or error differed"
        );
    }
}