haematite-cli 0.21.0

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
use serde_json::Value;

use super::super::{Stage, pipeline};
use super::Direction;
use crate::serve::query::words::{self, Role, Status};

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

#[test]
fn match_word_describes_the_labelled_graph_grammar() -> TestResult {
    let word = words::WORDS
        .iter()
        .find(|word| word.name == "match")
        .ok_or("match word missing")?;
    assert_eq!(word.role, Role::Source);
    assert_eq!(word.status, Status::Served);
    assert_eq!(
        word.arguments,
        &[
            "nodes: (binding:collection) or up to eight labelled nodes joined by directed relations",
            "binding: distinct names matching [a-z_][a-z0-9_]*",
            "relation: -[:kind]-> or <-[:kind]- at each of up to seven hops",
        ]
    );
    assert_eq!(
        word.meaning,
        "Match a labelled collection or a fixed chain of directed relations first or immediately after its collection."
    );
    assert!(word.refusal.is_none());
    let fixture: Vec<Value> = serde_json::from_str(include_str!("../words.json"))?;
    let fixture_word = fixture
        .iter()
        .find(|word| word["name"] == "match")
        .ok_or("match fixture word missing")?;
    assert_eq!(*fixture_word, serde_json::to_value(word)?);
    assert_eq!(
        words::WORDS
            .iter()
            .filter(|word| word.name == "match")
            .count(),
        1
    );
    Ok(())
}

#[test]
fn match_library_examples_have_complete_defaults_and_exact_graph_shapes() -> TestResult {
    let group: Value = serde_json::from_str(include_str!("../library/match.json"))?;
    assert_eq!(group["group"], "Matching relationships");
    let entries = group["entries"].as_array().ok_or("entries missing")?;
    assert_eq!(entries.len(), 4);
    for (entry, (name, labels, directions, kinds)) in entries.iter().zip([
        ("Labelled patients", vec!["patients"], vec![], vec![]),
        (
            "Patients admitted to wards",
            vec!["patients", "wards"],
            vec![Direction::Outgoing],
            vec!["admitted_to"],
        ),
        (
            "Wards admitting patients",
            vec!["wards", "patients"],
            vec![Direction::Incoming],
            vec!["admitted_to"],
        ),
        (
            "Patients with ward nurses",
            vec!["patients", "wards", "nurses"],
            vec![Direction::Outgoing, Direction::Incoming],
            vec!["admitted_to", "staffed_by"],
        ),
    ]) {
        assert_eq!(entry["name"], name);
        let mut query = entry["query"].as_str().ok_or("query missing")?.to_owned();
        let change = entry["change"].as_str().ok_or("change missing")?;
        assert!(
            !entry["sentence"]
                .as_str()
                .ok_or("sentence missing")?
                .is_empty()
        );
        for (key, value) in entry["defaults"].as_object().ok_or("defaults missing")? {
            let token = format!("${{{key}}}");
            let value = value.as_str().ok_or("default is not a string")?;
            assert!(!value.is_empty(), "{name}: {key}");
            assert!(query.contains(&token), "{name}: unused default {key}");
            assert!(change.contains(&token), "{name}: absent change {key}");
            query = query.replace(&token, value);
        }
        assert!(!query.contains("${"), "{name}: unresolved query {query}");
        let plan = pipeline(&query).map_err(|error| error.to_string())?;
        assert_eq!(plan.stages.len(), 1, "{name}");
        let stage = plan.stages.first().ok_or("match stage missing")?;
        assert_eq!(stage.span.start, 0);
        assert_eq!(stage.span.end, query.len());
        let Stage::Match { nodes, relations } = &stage.value else {
            return Err(format!("{name}: example did not parse as match").into());
        };
        assert_eq!(
            nodes
                .iter()
                .map(|node| node.label.value.as_str())
                .collect::<Vec<_>>(),
            labels
        );
        assert_eq!(
            relations
                .iter()
                .map(|relation| relation.direction)
                .collect::<Vec<_>>(),
            directions
        );
        assert_eq!(
            relations
                .iter()
                .map(|relation| relation.kind.value.as_str())
                .collect::<Vec<_>>(),
            kinds
        );
    }
    Ok(())
}