granit-parser 1.1.0

A YAML parser with comment and style support, written in pure Rust
Documentation
use granit_parser::{ErrorKind, Event, Options, Parser, StrInput};

fn assert_comment_free_value(events: &[(Event<'_>, granit_parser::Span)]) {
    assert!(events
        .iter()
        .all(|(event, _)| !matches!(event, Event::Comment(..))));
    assert!(events
        .iter()
        .any(|(event, _)| { matches!(event, Event::Scalar(value, ..) if value == "value") }));
}

#[test]
fn options_macro_starts_with_defaults_and_applies_fields() {
    let defaults = Options::default();
    assert!(defaults.emit_comments);
    assert_eq!(defaults.max_buffered_comment_events, 96);
    assert_eq!(defaults.simple_key_max_lookahead, 1024);
    assert_eq!(defaults.flow_nesting_limit, 255);
    assert_eq!(granit_parser::options! {}, defaults);

    let options = granit_parser::options! {
        emit_comments: false,
        max_buffered_comment_events: 7,
        simple_key_max_lookahead: 11,
        flow_nesting_limit: 13,
    };

    assert!(!options.emit_comments);
    assert_eq!(options.max_buffered_comment_events, 7);
    assert_eq!(options.simple_key_max_lookahead, 11);
    assert_eq!(options.flow_nesting_limit, 13);
}

#[test]
fn options_macro_configures_parser() {
    let options = granit_parser::options! {
        flow_nesting_limit: 1,
    };
    let error = Parser::with_options(StrInput::new("[[]]"), options)
        .find_map(Result::err)
        .expect("macro-configured flow limit should be enforced");

    assert_eq!(error.kind(), &ErrorKind::RecursionLimitExceeded);
}

#[test]
fn new_uses_default_options() {
    let yaml = "root: [a, {b: c}]\n";
    let from_new: Vec<_> = Parser::new(StrInput::new(yaml)).collect();
    let from_options: Vec<_> =
        Parser::with_options(StrInput::new(yaml), Options::default()).collect();

    assert_eq!(from_new, from_options);
}

#[test]
fn common_input_constructors_accept_options() {
    let yaml = String::from("# ignored\nvalue\n");
    let options = granit_parser::options! {
        emit_comments: false,
    };

    let string_events = Parser::new_from_str_with_options(&yaml, options.clone())
        .collect::<Result<Vec<_>, _>>()
        .expect("string constructor should parse with custom options");
    assert_comment_free_value(&string_events);

    let iterator_events = Parser::new_from_iter_with_options(yaml.chars(), options.clone())
        .collect::<Result<Vec<_>, _>>()
        .expect("iterator constructor should parse with custom options");
    assert_comment_free_value(&iterator_events);

    let fallible_events =
        Parser::new_from_fallible_iter_with_options(yaml.chars().map(Ok), options)
            .collect::<Result<Vec<_>, _>>()
            .expect("fallible iterator constructor should parse with custom options");
    assert_comment_free_value(&fallible_events);
}

#[test]
fn buffered_comment_limit_can_be_raised() {
    let default_limit = Options::default().max_buffered_comment_events;
    let raised_limit = default_limit + 1;
    let mut yaml = String::from("key: # c0\n");
    for index in 1..=default_limit {
        yaml.push_str("# c");
        yaml.push_str(&index.to_string());
        yaml.push('\n');
    }
    yaml.push_str("next: value\n");

    let error = Parser::new(StrInput::new(&yaml))
        .find_map(Result::err)
        .expect("default options should reject the comment above their limit");
    assert_eq!(error.kind(), &ErrorKind::TooManyComments);

    let options = granit_parser::options! {
        max_buffered_comment_events: raised_limit,
    };
    let events = Parser::with_options(StrInput::new(&yaml), options)
        .collect::<Result<Vec<_>, _>>()
        .expect("custom options should accept the raised buffered-comment limit");

    assert_eq!(
        events
            .iter()
            .filter(|(event, _)| matches!(event, Event::Comment(..)))
            .count(),
        raised_limit
    );
}

#[test]
fn buffered_comment_limit_honors_lower_and_zero_boundaries() {
    let one_comment = "key: # one\nnext: value\n";
    let options = granit_parser::options! {
        max_buffered_comment_events: 1,
    };
    Parser::with_options(StrInput::new(one_comment), options)
        .collect::<Result<Vec<_>, _>>()
        .expect("one buffered comment should be accepted at limit one");

    let two_comments = "key: # one\n# two\nnext: value\n";
    let options = granit_parser::options! {
        max_buffered_comment_events: 1,
    };
    let error = Parser::with_options(StrInput::new(two_comments), options)
        .find_map(Result::err)
        .expect("the second buffered comment should exceed limit one");
    assert_eq!(error.kind(), &ErrorKind::TooManyComments);

    let options = granit_parser::options! {
        max_buffered_comment_events: 0,
    };
    let error = Parser::with_options(StrInput::new(one_comment), options)
        .find_map(Result::err)
        .expect("the first buffered comment should exceed limit zero");
    assert_eq!(error.kind(), &ErrorKind::TooManyComments);
}

#[test]
fn zero_comment_limit_still_precedes_indentless_sequence_start() {
    let options = granit_parser::options! {
        max_buffered_comment_events: 0,
    };
    let mut events = Vec::new();
    let error = Parser::with_options(StrInput::new("a:\n- # one\n"), options)
        .find_map(|result| match result {
            Ok((event, _)) => {
                events.push(event);
                None
            }
            Err(error) => Some(error),
        })
        .expect("the first buffered comment should exceed limit zero");

    assert_eq!(error.kind(), &ErrorKind::TooManyComments);
    assert!(
        events
            .iter()
            .all(|event| !matches!(event, Event::SequenceStart(..))),
        "the resource-limit error must not be deferred behind the sequence start"
    );
}

#[test]
fn simple_key_lookahead_can_be_raised() {
    let key = "k".repeat(1025);
    let yaml = format!("a: b\n{key}: value\n");

    let error = Parser::new(StrInput::new(&yaml))
        .find_map(Result::err)
        .expect("default options should reject a 1025-character simple key");
    assert_eq!(error.kind(), &ErrorKind::SimpleKeyExpected);

    let options = granit_parser::options! {
        simple_key_max_lookahead: 1025,
    };
    Parser::with_options(StrInput::new(&yaml), options)
        .collect::<Result<Vec<_>, _>>()
        .expect("custom options should accept a 1025-character simple key");
}

#[test]
fn simple_key_lookahead_honors_configured_boundary() {
    let yaml = "a: b\nlong: value\n";
    let options = granit_parser::options! {
        simple_key_max_lookahead: 4,
    };
    Parser::with_options(StrInput::new(yaml), options)
        .collect::<Result<Vec<_>, _>>()
        .expect("a simple key at the configured limit should be accepted");

    let options = granit_parser::options! {
        simple_key_max_lookahead: 3,
    };
    let error = Parser::with_options(StrInput::new(yaml), options)
        .find_map(Result::err)
        .expect("a simple key beyond the configured limit should be rejected");
    assert_eq!(error.kind(), &ErrorKind::SimpleKeyExpected);
}

#[test]
fn flow_nesting_limit_can_be_raised_above_previous_type_limit() {
    let yaml = format!("{}{}", "[".repeat(256), "]".repeat(256));

    let error = Parser::new(StrInput::new(&yaml))
        .find_map(Result::err)
        .expect("default options should reject flow nesting level 256");
    assert_eq!(error.kind(), &ErrorKind::RecursionLimitExceeded);

    let options = granit_parser::options! {
        flow_nesting_limit: 256,
    };
    let events = Parser::with_options(StrInput::new(&yaml), options)
        .collect::<Result<Vec<_>, _>>()
        .expect("custom options should accept flow nesting level 256");

    assert_eq!(
        events
            .iter()
            .filter(|(event, _)| matches!(event, Event::SequenceStart(..)))
            .count(),
        256
    );
}

#[test]
fn flow_nesting_limit_honors_lower_and_zero_boundaries() {
    let options = granit_parser::options! {
        flow_nesting_limit: 1,
    };
    Parser::with_options(StrInput::new("[]"), options)
        .collect::<Result<Vec<_>, _>>()
        .expect("one flow collection should be accepted at limit one");

    let options = granit_parser::options! {
        flow_nesting_limit: 1,
    };
    let error = Parser::with_options(StrInput::new("[[]]"), options)
        .find_map(Result::err)
        .expect("the second flow collection should exceed limit one");
    assert_eq!(error.kind(), &ErrorKind::RecursionLimitExceeded);

    let options = granit_parser::options! {
        flow_nesting_limit: 0,
    };
    let error = Parser::with_options(StrInput::new("[]"), options)
        .find_map(Result::err)
        .expect("the first flow collection should exceed limit zero");
    assert_eq!(error.kind(), &ErrorKind::RecursionLimitExceeded);
}