haematite-cli 0.21.8

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use std::collections::{BTreeMap, BTreeSet};
use std::num::NonZeroU64;
use std::path::PathBuf;

use serde::Deserialize;
use serde_json::Value;

use super::support::{Fixture, request};
use crate::serve::protocol::{QueryRequest, Selection, Version};

type Error = Box<dyn std::error::Error>;

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Group {
    pub group: String,
    pub entries: Vec<Entry>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Entry {
    pub name: String,
    pub sentence: String,
    pub query: String,
    pub change: String,
    pub defaults: BTreeMap<String, Default>,
    pub selection: Option<Selected>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Selected {
    branch: String,
    version: String,
}

#[derive(Deserialize)]
#[serde(untagged)]
pub enum Default {
    Literal(String),
    Captured(Context),
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Context {
    context: String,
}

pub fn load() -> Result<Vec<Group>, Error> {
    let directory = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/serve/query/library");
    let mut paths = Vec::new();
    for entry in std::fs::read_dir(&directory)? {
        let path = entry?.path();
        if path
            .extension()
            .is_some_and(|extension| extension == "json")
        {
            paths.push(path);
        }
    }
    paths.sort();
    if paths.is_empty() {
        return Err(format!("library {} has no groups", directory.display()).into());
    }
    let mut groups = Vec::new();
    for path in paths {
        let bytes = std::fs::read(&path)?;
        let group = serde_json::from_slice(&bytes)
            .map_err(|error| format!("library {}: {error}", path.display()))?;
        groups.push(group);
    }
    Ok(groups)
}

pub fn render(entry: &Entry, captured: &Value) -> Result<QueryRequest, Error> {
    render_inner(entry, captured).map_err(|error| format!("entry {}: {error}", entry.name).into())
}

fn render_inner(entry: &Entry, captured: &Value) -> Result<QueryRequest, Error> {
    for (kind, text) in [
        ("name", &entry.name),
        ("sentence", &entry.sentence),
        ("change", &entry.change),
    ] {
        if text.trim().is_empty() {
            return Err(format!("missing {kind}").into());
        }
    }
    let mut defaults = BTreeMap::new();
    for (part, default) in &entry.defaults {
        if part.is_empty()
            || !part
                .chars()
                .all(|character| character.is_ascii_lowercase() || character == '_')
        {
            return Err(format!("invalid placeholder {part:?}").into());
        }
        let value = match default {
            Default::Literal(value) => value.clone(),
            Default::Captured(context) if context.context == "selection.version" => {
                let value = captured
                    .get("version")
                    .ok_or("missing selection.version default")?;
                let version: Version = serde_json::from_value(value.clone())?;
                serde_json::to_string(&version)?
            }
            Default::Captured(context) => {
                return Err(format!("unknown default context {}", context.context).into());
            }
        };
        if value.is_empty() || value.contains("${") {
            return Err(format!("placeholder {part} has an empty or unresolved default").into());
        }
        defaults.insert(part.clone(), value);
    }
    let mut used = BTreeSet::new();
    let query = expand(&entry.query, &defaults, &mut used)?;
    crate::serve::query::parser::pipeline(&query)
        .map_err(|refusal| format!("{}: {refusal}", refusal.code()))?;
    expand(&entry.change, &defaults, &mut used)?;
    let mut ask = request(&query, NonZeroU64::MIN.saturating_add(3));
    if let Some(selected) = &entry.selection {
        let branch = expand(&selected.branch, &defaults, &mut used)?;
        let version = expand(&selected.version, &defaults, &mut used)?;
        ask.selection = Selection {
            branch,
            version: Some(serde_json::from_str(&version)?),
        };
    }
    let declared: BTreeSet<_> = defaults.keys().cloned().collect();
    if used != declared {
        return Err(format!(
            "unused defaults: {:?}",
            declared.difference(&used).collect::<Vec<_>>()
        )
        .into());
    }
    Ok(ask)
}

fn expand(
    text: &str,
    defaults: &BTreeMap<String, String>,
    used: &mut BTreeSet<String>,
) -> Result<String, Error> {
    let mut remaining = text;
    let mut expanded = String::new();
    while let Some((before, after)) = remaining.split_once("${") {
        expanded.push_str(before);
        let (part, rest) = after.split_once('}').ok_or("unclosed placeholder")?;
        let default = defaults
            .get(part)
            .ok_or_else(|| format!("placeholder {part} has no default"))?;
        used.insert(part.to_owned());
        expanded.push_str(default);
        remaining = rest;
    }
    expanded.push_str(remaining);
    Ok(expanded)
}

pub fn run_entry(fixture: &Fixture, entry: &Entry, captured: &Value) -> Result<Value, Error> {
    let ask = render(entry, captured)?;
    let answer = fixture.run(&ask, &[]).map_err(|failure| {
        format!(
            "entry {}: {}: {}",
            entry.name, failure.code, failure.message
        )
    })?;
    let rows = answer
        .get("rows")
        .and_then(Value::as_array)
        .ok_or_else(|| format!("entry {}: query.run returned no page rows", entry.name))?;
    for row in rows {
        if row.get("value").is_none() || row.get("origin").is_none() {
            return Err(format!(
                "entry {}: query.run returned a row without value or origin",
                entry.name
            )
            .into());
        }
    }
    let selection = answer
        .get("selection")
        .ok_or_else(|| format!("entry {}: query.run returned no selection", entry.name))?;
    // Captured roots describe the page; Selection is the request vocabulary.
    let mut selected = selection
        .as_object()
        .ok_or_else(|| format!("entry {}: page selection is not an object", entry.name))?
        .clone();
    let roots = selected
        .remove("roots")
        .ok_or_else(|| format!("entry {}: page selection has no captured roots", entry.name))?;
    if !roots.is_array() {
        return Err(format!("entry {}: captured roots are not an array", entry.name).into());
    }
    serde_json::from_value::<Selection>(Value::Object(selected))
        .map_err(|error| format!("entry {}: invalid page selection: {error}", entry.name))?;
    Ok(answer)
}