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::Deserialize;
use serde_json::value::RawValue;

use super::ServiceRefusal;
use super::read_support::failure;
use crate::mcp::types::{self, ReviewValue};

#[derive(Deserialize)]
pub(super) struct Difference {
    collection: String,
    document_id: Option<String>,
    what: String,
    base: String,
    here: String,
}

#[derive(Deserialize)]
pub(super) struct Conflict {
    collection: String,
    document_id: Option<String>,
    what: String,
    ancestor: String,
    target: String,
    source: String,
    state: types::ConflictState,
    choice_token: Option<String>,
}

#[derive(Deserialize)]
struct Edge {
    relation: String,
    from: types::RecordRef,
    to: types::RecordRef,
}

fn invalid(message: &str) -> ServiceRefusal {
    failure("mcp_review_owner_invalid", message)
}

fn record(collection: String, id: Option<String>) -> Result<types::RecordRef, ServiceRefusal> {
    Ok(types::RecordRef {
        collection,
        document_id: id
            .ok_or_else(|| invalid("The served owner requires a document identifier"))?,
    })
}

fn declaration(id: Option<&str>) -> Result<(), ServiceRefusal> {
    if id.is_none() {
        Ok(())
    } else {
        Err(invalid(
            "The served declaration unexpectedly has a document identifier",
        ))
    }
}

fn edge(
    collection: &str,
    id: Option<&str>,
    relation: &str,
    values: [&str; 3],
) -> Result<types::ReviewOwner, ServiceRefusal> {
    let value = values
        .into_iter()
        .find(|value| !value.is_empty())
        .ok_or_else(|| invalid("The served edge has no value"))?;
    let edge: Edge = serde_json::from_str(value)
        .map_err(|error| failure("mcp_service_answer_invalid", error.to_string()))?;
    if edge.relation != relation
        || edge.from.collection != collection
        || Some(edge.from.document_id.as_str()) != id
    {
        return Err(invalid("The served edge value does not identify its owner"));
    }
    Ok(types::ReviewOwner::Edge {
        relation: edge.relation,
        from: edge.from,
        to: edge.to,
    })
}

fn owner(
    collection: String,
    id: Option<String>,
    what: &str,
    values: [&str; 3],
) -> Result<types::ReviewOwner, ServiceRefusal> {
    if what == "collection" {
        declaration(id.as_deref())?;
        return Ok(types::ReviewOwner::Collection { collection });
    }
    if what == "document" {
        return Ok(types::ReviewOwner::Document {
            record: record(collection, id)?,
        });
    }
    if what == "relation" {
        declaration(id.as_deref())?;
        return Ok(types::ReviewOwner::Relation {
            relation: collection,
        });
    }
    if let Some(relation) = what.strip_prefix("edge ") {
        return edge(&collection, id.as_deref(), relation, values);
    }
    if let Some(field) = what.strip_prefix("vector field ") {
        declaration(id.as_deref())?;
        return Ok(types::ReviewOwner::VectorField {
            collection,
            field: field.to_owned(),
        });
    }
    if let Some(field) = what.strip_prefix("vector ") {
        return Ok(types::ReviewOwner::Vector {
            record: record(collection, id)?,
            field: field.to_owned(),
        });
    }
    Err(invalid(
        "The service returned an unrepresentable review owner",
    ))
}

fn value(value: String) -> Result<ReviewValue, ServiceRefusal> {
    if value.is_empty() {
        return Ok(ReviewValue::Absent);
    }
    RawValue::from_string(value)
        .map(|value| ReviewValue::Value { value })
        .map_err(|error| failure("mcp_service_answer_invalid", error.to_string()))
}

pub(super) fn difference(line: Difference) -> Result<types::ReviewLine, ServiceRefusal> {
    let owner = owner(
        line.collection,
        line.document_id,
        &line.what,
        [&line.here, &line.base, ""],
    )?;
    let before = value(line.base)?;
    let after = value(line.here)?;
    let kind = match (&before, &after) {
        (ReviewValue::Absent, ReviewValue::Value { .. }) => types::DifferenceKind::Added,
        (ReviewValue::Value { .. }, ReviewValue::Absent) => types::DifferenceKind::Removed,
        (ReviewValue::Value { .. }, ReviewValue::Value { .. }) => types::DifferenceKind::Changed,
        (ReviewValue::Absent, ReviewValue::Absent) => {
            return Err(invalid("The served difference has neither value"));
        }
    };
    Ok(types::ReviewLine {
        kind,
        owner,
        before,
        after,
    })
}

pub(super) fn conflict(line: Conflict) -> Result<types::ConflictLine, ServiceRefusal> {
    let owner = owner(
        line.collection,
        line.document_id,
        &line.what,
        [&line.source, &line.target, &line.ancestor],
    )?;
    Ok(types::ConflictLine {
        owner,
        ancestor: value(line.ancestor)?,
        target: value(line.target)?,
        source: value(line.source)?,
        state: line.state,
        choice_token: line.choice_token,
    })
}