haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! Authoritative equality declarations, independent of derived memberships.

use haematite::db::ReadOnlyDatabase;
use serde::{Deserialize, Serialize};

use super::{Captured, Failure, embedded, identifier, mutations::PreparedWrite, reads};
use crate::serve::protocol::{Change, IndexDeclaration, Version};

#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Recorded {
    format: u32,
    change: Change,
    declared_in: String,
    declared_at: Version,
}

pub(in crate::serve) fn validate_names(collection: &str, field: &str) -> Result<(), Failure> {
    identifier(collection)?;
    if field.contains(['.', '/', '[', ']']) {
        return Err(Failure::new(
            "index_field_not_top_level",
            "an equality field must name one top-level member without path syntax",
        ));
    }
    identifier(field)
}

pub(super) fn declare(
    reader: &ReadOnlyDatabase,
    at: &Captured,
    write: &IndexDeclaration,
    next: Version,
) -> Result<Vec<PreparedWrite>, Failure> {
    validate_names(&write.collection, &write.field)?;
    if reads::collection(reader, at, &write.collection)?.is_none() {
        return Err(Failure::new("collection_missing", &write.collection));
    }
    if declared(reader, at, &write.collection, &write.field)? {
        return Err(Failure::new(
            "index_already_declared",
            "this collection field already has an equality declaration",
        ));
    }
    let vector_key = format!("w\0{}\0{}", write.collection, write.field);
    if let Some(bytes) = reads::get_bytes(reader, at, vector_key.as_bytes())? {
        embedded::read_field(&bytes)?;
        return Err(Failure::new(
            "index_vector_field",
            "a vector field cannot be declared for equality lookup",
        ));
    }
    let row = Recorded {
        format: 1,
        change: Change::IndexDeclare(write.clone()),
        declared_in: at.branch.clone(),
        declared_at: next,
    };
    Ok(vec![(
        declaration_key(&write.collection, &write.field),
        Some(serde_json::to_vec(&row).map_err(Failure::storage)?),
    )])
}

pub(super) fn require_non_vector(
    reader: &ReadOnlyDatabase,
    at: &Captured,
    collection: &str,
    field: &str,
) -> Result<(), Failure> {
    if declared(reader, at, collection, field)? {
        return Err(Failure::new(
            "index_vector_field",
            "an equality field cannot become a vector field",
        ));
    }
    Ok(())
}

fn declared(
    reader: &ReadOnlyDatabase,
    at: &Captured,
    collection: &str,
    field: &str,
) -> Result<bool, Failure> {
    let Some(bytes) = reads::get_bytes(reader, at, &declaration_key(collection, field))? else {
        return Ok(false);
    };
    decode(collection, field, &bytes)?;
    Ok(true)
}

pub(super) fn render(collection: &str, field: &str, bytes: &[u8]) -> Result<String, Failure> {
    serde_json::to_string(&decode(collection, field, bytes)?).map_err(Failure::storage)
}

fn decode(collection: &str, field: &str, bytes: &[u8]) -> Result<Recorded, Failure> {
    let corrupt = |message| Failure::new("index_declaration_corrupt", message);
    let row: Recorded =
        serde_json::from_slice(bytes).map_err(|error| corrupt(error.to_string()))?;
    let Change::IndexDeclare(declaration) = &row.change else {
        return Err(corrupt(
            "declaration row has another change kind".to_owned(),
        ));
    };
    if row.format != 1 || declaration.collection != collection || declaration.field != field {
        return Err(corrupt(
            "declaration identity differs from its key".to_owned(),
        ));
    }
    validate_names(collection, field).map_err(|error| corrupt(error.to_string()))?;
    identifier(&row.declared_in).map_err(|error| corrupt(error.to_string()))?;
    Ok(row)
}

fn declaration_key(collection: &str, field: &str) -> Vec<u8> {
    format!("f\0eq\0{collection}\0{field}").into_bytes()
}

#[cfg(test)]
mod tests {
    use super::decode;
    use crate::serve::protocol::Version;
    use serde_json::json;

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

    #[test]
    fn index_declaration_decode_refuses_corrupt_identity_and_shape() -> TestResult {
        let valid = json!({
            "format":1,
            "change":{"kind":"index.declare", "value":{"collection":"notes", "field":"ward"}},
            "declared_in":"main", "declared_at":Version {generation:7, sequence:2}
        });
        decode("notes", "ward", &serde_json::to_vec(&valid)?)?;
        let mut unsupported = valid.clone();
        unsupported["format"] = json!(2);
        let mut wrong_field = valid.clone();
        wrong_field["change"]["value"]["field"] = json!("other");
        let mut wrong_kind = valid.clone();
        wrong_kind["change"] = json!({"kind":"relation.delete", "value":{"relation":"ward"}});
        let mut authorless = valid.clone();
        authorless["declared_in"] = json!("");
        let mut extra = valid;
        extra["members"] = json!([]);
        for corrupt in [unsupported, wrong_field, wrong_kind, authorless, extra] {
            let failure = decode("notes", "ward", &serde_json::to_vec(&corrupt)?)
                .err()
                .ok_or("corrupt declaration accepted")?;
            assert_eq!(failure.code, "index_declaration_corrupt");
        }
        Ok(())
    }
}