haematite-cli 0.21.3

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use super::{Source, Span, Stage, pipeline};

type TestResult = Result<(), Box<dyn std::error::Error>>;

fn read(text: &str) -> Result<super::Plan, String> {
    pipeline(text).map_err(|error| error.to_string())
}

#[test]
fn later_shape_has_its_own_stage_and_span() -> TestResult {
    let text = "patients | { name ward { code } } | limit(2)";
    let plan = read(text)?;
    assert_eq!(plan.stages.len(), 2);
    assert_eq!(plan.stages[0].span, Span { start: 11, end: 33 });
    assert!(!matches!(plan.stages[0].value, Stage::Jq(_)));
    assert!(matches!(plan.stages[1].value, Stage::Limit(limit) if limit.get() == 2));
    Ok(())
}

#[test]
fn first_shape_names_the_collection_and_its_projection() -> TestResult {
    let text = "  { patients { name ward } } | '.'";
    let plan = read(text)?;
    assert_eq!(
        plan.source,
        Source::Collection {
            name: "patients".to_owned(),
            arguments: Vec::new()
        }
    );
    assert_eq!(plan.stages.len(), 2);
    assert_eq!(plan.stages[0].span, Span { start: 2, end: 28 });
    assert!(!matches!(plan.stages[0].value, Stage::Jq(_)));
    assert_eq!(plan.stages[1].value, Stage::Jq(".".to_owned()));
    Ok(())
}

#[test]
fn two_collection_roots_keep_their_selections_and_spans() -> TestResult {
    use haematite_query::stage::shape::{Field, Selection};

    use super::Root;

    let plan = read("{ patients { name } wards { code } }")?;
    assert_eq!(
        plan.source,
        Source::Shaped(vec![
            Root {
                name: "patients".to_owned(),
                arguments: Vec::new(),
                selection: Selection::new(vec![Field {
                    name: "name".to_owned(),
                    selection: None,
                }])?,
                span: Span { start: 2, end: 19 },
            },
            Root {
                name: "wards".to_owned(),
                arguments: Vec::new(),
                selection: Selection::new(vec![Field {
                    name: "code".to_owned(),
                    selection: None,
                }])?,
                span: Span { start: 20, end: 34 },
            },
        ])
    );
    assert!(plan.stages.is_empty());
    Ok(())
}

#[test]
fn repeated_collection_root_is_named_at_the_second_root() -> TestResult {
    let error = pipeline("{ patients { name } patients { ward } }")
        .err()
        .ok_or("repeated root accepted")?;
    assert_eq!(error.code(), "query_parse");
    assert!(
        error
            .to_string()
            .contains("a shape repeats the root \"patients\"")
    );
    assert_eq!(error.span(), Span { start: 20, end: 37 });
    Ok(())
}

#[test]
fn match_after_several_shaped_roots_is_refused_by_name() -> TestResult {
    let query = "{ patients { name } wards { code } } | match (p:patients)";
    let error = pipeline(query).err().ok_or("match accepted")?;
    assert_eq!(error.code(), "query_stage_not_available");
    assert!(
        error
            .to_string()
            .contains("`match` after a shape with several roots")
    );
    assert_eq!(
        error.span(),
        Span {
            start: 39,
            end: query.len()
        }
    );
    Ok(())
}

#[test]
fn shaped_roots_reuse_each_collections_argument_reader() -> TestResult {
    let plan =
        read("{ patients(ward: \"4B\" and admitted: 2) { name } wards(code: \"4B\") { code } }")?;
    let Source::Shaped(roots) = plan.source else {
        return Err("missing shaped source".into());
    };
    assert_eq!(roots.len(), 2);
    assert_eq!(
        roots[0].arguments,
        vec![
            super::Argument {
                field: "ward".to_owned(),
                value: serde_json::json!("4B"),
            },
            super::Argument {
                field: "admitted".to_owned(),
                value: serde_json::json!(2),
            },
        ]
    );
    assert_eq!(
        roots[1].arguments,
        vec![super::Argument {
            field: "code".to_owned(),
            value: serde_json::json!("4B"),
        }]
    );
    Ok(())
}

#[test]
fn a_brace_inside_quotes_is_jq() -> TestResult {
    let plan = read("patients | '{ name: .name, ward: { code: .ward } }'")?;
    assert_eq!(plan.stages.len(), 1);
    assert_eq!(
        plan.stages[0].value,
        Stage::Jq("{ name: .name, ward: { code: .ward } }".to_owned())
    );
    let plan = read("'{ patients: . }'")?;
    assert_eq!(plan.source, Source::All);
    assert_eq!(
        plan.stages[0].value,
        Stage::Jq("{ patients: . }".to_owned())
    );
    Ok(())
}

#[test]
fn malformed_shapes_are_parse_refusals() -> TestResult {
    for text in [
        "patients | { }",
        "patients | { name",
        "patients | { name name }",
        "patients | { ward { } }",
        "patients | { name, ward }",
        "{ patients }",
        "{ }",
        "{ patients { name }",
    ] {
        let error = pipeline(text).err().ok_or("malformed shape accepted")?;
        assert_eq!(error.code(), "query_parse", "{text}: {error}");
    }
    Ok(())
}

#[test]
fn unicode_whitespace_retains_byte_positions() -> TestResult {
    let text = "patients | \u{2003}{ name\u{2003}ward { code } } | count";
    let plan = read(text)?;
    let start = text.find('{').ok_or("missing brace")?;
    let end = text.rfind('}').ok_or("missing closing brace")? + 1;
    assert_eq!(plan.stages[0].span, Span { start, end });
    assert!(matches!(plan.stages[1].value, Stage::Count));
    Ok(())
}

#[test]
fn shape_word_and_query_checker_share_the_served_brace_spans() -> TestResult {
    use crate::serve::query::check::{CheckRequest, WordsRequest, check, words};
    use serde_json::json;
    let vocabulary = words(&WordsRequest {})?;
    let word = vocabulary
        .as_array()
        .ok_or("missing vocabulary")?
        .iter()
        .find(|word| word.get("name") == Some(&json!("{")))
        .ok_or("missing brace word")?;
    assert_eq!(
        word.get("status").ok_or("missing status")?,
        &json!("served")
    );
    assert_eq!(word.get("role").ok_or("missing role")?, &json!("shape"));
    let query = "{ patients { name } } | { name }";
    let answer = check(&CheckRequest {
        query: query.to_owned(),
    });
    assert!(answer.get("refusal").ok_or("missing refusal")?.is_null());
    assert_eq!(
        answer.get("stages").ok_or("missing checked stages")?,
        &json!([
            {"word":"{", "span":{"start":0, "end":21}},
            {"word":"{", "span":{"start":24, "end":32}}
        ])
    );
    Ok(())
}

#[test]
fn excessive_nested_braces_are_refused_before_recursive_execution() -> TestResult {
    let text = format!("patients | {}name{}", "{ ward ".repeat(65), " }".repeat(65));
    let error = pipeline(&text).err().ok_or("unbounded grammar accepted")?;
    assert_eq!(error.code(), "query_parse");
    assert!(error.to_string().contains("64 selection levels"));
    Ok(())
}