tstring-yaml 0.2.1

YAML parser-first renderer for t-string structured data backends
Documentation
use tstring_syntax::{TemplateInput, TemplateInterpolation, TemplateSegment};
use tstring_yaml::{
    YamlValueNode, check_template, format_template, parse_template, validate_template,
};

fn interpolation(index: usize, expression: &str) -> TemplateSegment {
    TemplateSegment::Interpolation(TemplateInterpolation {
        expression: expression.to_owned(),
        conversion: None,
        format_spec: String::new(),
        interpolation_index: index,
        raw_source: None,
    })
}

#[test]
fn parses_yaml_mapping_with_interpolated_scalar_value() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("name: ".to_owned()),
        interpolation(0, "name"),
        TemplateSegment::StaticText("\n".to_owned()),
    ]);

    let stream = parse_template(&template).expect("expected YAML parse success");
    assert_eq!(stream.documents.len(), 1);
    assert!(matches!(
        stream.documents[0].value,
        YamlValueNode::Mapping(_)
    ));
}

#[test]
fn yaml_parse_errors_include_spans() {
    let template =
        TemplateInput::from_segments(vec![TemplateSegment::StaticText("a:\n\t- 1\n".to_owned())]);

    let error = parse_template(&template).expect_err("expected YAML parse failure");
    assert_eq!(error.diagnostics[0].code, "yaml.parse");
    assert!(error.diagnostics[0].span.is_some());
}

#[test]
fn checks_valid_yaml_templates() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("name: ".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "name".to_owned(),
            conversion: None,
            format_spec: String::new(),
            interpolation_index: 0,
            raw_source: Some("{name}".to_owned()),
        }),
        TemplateSegment::StaticText("\nmeta:\n  team: \"".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "team".to_owned(),
            conversion: Some("s".to_owned()),
            format_spec: String::new(),
            interpolation_index: 1,
            raw_source: Some("{team!s}".to_owned()),
        }),
        TemplateSegment::StaticText("\"\n".to_owned()),
    ]);

    check_template(&template).expect("expected check success");
}

#[test]
fn rejects_plain_scalars_that_mix_whitespace_and_interpolation() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("replicas: fdsa fff fds".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "replicas".to_owned(),
            conversion: None,
            format_spec: String::new(),
            interpolation_index: 0,
            raw_source: Some("{replicas}".to_owned()),
        }),
        TemplateSegment::StaticText("\n".to_owned()),
    ]);

    let error = check_template(&template).expect_err("expected YAML validation failure");
    assert_eq!(error.diagnostics[0].code, "yaml.parse");
    assert!(
        error
            .message
            .contains("Quote YAML plain scalars that mix whitespace and interpolations")
    );
}

#[test]
fn validates_token_like_plain_scalars_with_interpolation() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("plain: item-".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "user".to_owned(),
            conversion: None,
            format_spec: String::new(),
            interpolation_index: 0,
            raw_source: Some("{user}".to_owned()),
        }),
        TemplateSegment::StaticText("\n".to_owned()),
    ]);

    validate_template(&template).expect("expected YAML validation success");
}

#[test]
fn validates_plain_scalars_with_multiple_interpolations_and_no_whitespace() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("plain: ".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "foo".to_owned(),
            conversion: None,
            format_spec: String::new(),
            interpolation_index: 0,
            raw_source: Some("{foo}".to_owned()),
        }),
        TemplateSegment::StaticText("-".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "bar".to_owned(),
            conversion: None,
            format_spec: String::new(),
            interpolation_index: 1,
            raw_source: Some("{bar}".to_owned()),
        }),
        TemplateSegment::StaticText("\n".to_owned()),
    ]);

    validate_template(&template).expect("expected YAML validation success");
}

#[test]
fn formats_yaml_templates_with_raw_interpolations() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("name: ".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "name".to_owned(),
            conversion: None,
            format_spec: String::new(),
            interpolation_index: 0,
            raw_source: Some("{name}".to_owned()),
        }),
        TemplateSegment::StaticText("\nmessage: \"Hello ".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "user".to_owned(),
            conversion: Some("r".to_owned()),
            format_spec: ">5".to_owned(),
            interpolation_index: 1,
            raw_source: Some("{user!r:>5}".to_owned()),
        }),
        TemplateSegment::StaticText("\"\nitems:\n  - ".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "item".to_owned(),
            conversion: None,
            format_spec: String::new(),
            interpolation_index: 2,
            raw_source: Some("{item}".to_owned()),
        }),
        TemplateSegment::StaticText("\n".to_owned()),
    ]);

    assert_eq!(
        format_template(&template).expect("expected format success"),
        "name: {name}\nmessage: \"Hello {user!r:>5}\"\nitems:\n  - {item}"
    );
}

#[test]
fn format_requires_raw_source_for_yaml_interpolations() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("name: ".to_owned()),
        interpolation(0, "name"),
        TemplateSegment::StaticText("\n".to_owned()),
    ]);

    let error = format_template(&template).expect_err("expected format failure");
    assert_eq!(error.kind, tstring_syntax::ErrorKind::Semantic);
    assert_eq!(error.diagnostics[0].code, "yaml.format");
}

#[test]
fn formats_explicit_complex_keys_as_flow_when_required() {
    let template = TemplateInput::from_segments(vec![TemplateSegment::StaticText(
        "?\n  a:\n    - 1\n    - 2\n: 1\n".to_owned(),
    )]);

    assert_eq!(
        format_template(&template).expect("expected format success"),
        "? { a: [ 1, 2 ] }\n: 1"
    );
}

#[test]
fn preserves_required_newline_for_empty_block_scalars() {
    let template =
        TemplateInput::from_segments(vec![TemplateSegment::StaticText("value: |\n".to_owned())]);

    assert_eq!(
        format_template(&template).expect("expected format success"),
        "value: |\n"
    );
}