tstring-json 0.2.1

JSON parser-first renderer for t-string structured data backends
Documentation
use tstring_json::{
    JsonKeyValue, JsonStringPart, JsonValueNode, check_template, format_template, parse_template,
    parse_validated_template, validate_template,
};
use tstring_syntax::{TemplateInput, TemplateInterpolation, TemplateSegment};

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_json_with_interpolated_key_and_value_segments() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("{\"name-".to_owned()),
        interpolation(0, "left"),
        TemplateSegment::StaticText("\": ".to_owned()),
        interpolation(1, "right"),
        TemplateSegment::StaticText("}".to_owned()),
    ]);

    let document = parse_template(&template).expect("expected JSON parse success");
    let JsonValueNode::Object(object) = document.value else {
        panic!("expected JSON object");
    };
    assert_eq!(object.members.len(), 1);
    let JsonKeyValue::String(key) = &object.members[0].key.value else {
        panic!("expected promoted JSON string key");
    };
    assert!(matches!(key.chunks[1], JsonStringPart::Interpolation(_)));
    assert!(matches!(
        object.members[0].value,
        JsonValueNode::Interpolation(_)
    ));
}

#[test]
fn json_parse_errors_include_spans() {
    let template = TemplateInput::from_segments(vec![TemplateSegment::StaticText(
        "{\"a\": 1 trailing}".to_owned(),
    )]);

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

#[test]
fn checks_valid_json_templates() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("{\"name\": ".to_owned()),
        interpolation(0, "name"),
        TemplateSegment::StaticText(", \"message\": \"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("\"}".to_owned()),
    ]);

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

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

    validate_template(&template).expect("expected validate success");
    parse_validated_template(&template).expect("expected validated parse success");
}

#[test]
fn formats_json_templates_with_raw_interpolations() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("{".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "key".to_owned(),
            conversion: None,
            format_spec: String::new(),
            interpolation_index: 0,
            raw_source: Some("{key}".to_owned()),
        }),
        TemplateSegment::StaticText(": ".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "value".to_owned(),
            conversion: None,
            format_spec: String::new(),
            interpolation_index: 1,
            raw_source: Some("{value}".to_owned()),
        }),
        TemplateSegment::StaticText(", \"greeting\": \"hi ".to_owned()),
        TemplateSegment::Interpolation(TemplateInterpolation {
            expression: "user".to_owned(),
            conversion: Some("r".to_owned()),
            format_spec: ">5".to_owned(),
            interpolation_index: 2,
            raw_source: Some("{user!r:>5}".to_owned()),
        }),
        TemplateSegment::StaticText("\"}".to_owned()),
    ]);

    assert_eq!(
        format_template(&template).expect("expected format success"),
        r#"{{key}: {value}, "greeting": "hi {user!r:>5}"}"#
    );
}

#[test]
fn format_requires_raw_source_for_interpolations() {
    let template = TemplateInput::from_segments(vec![
        TemplateSegment::StaticText("{\"name\": ".to_owned()),
        interpolation(0, "name"),
        TemplateSegment::StaticText("}".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, "json.format");
}