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
use std::sync::Arc;

use haematite::{
    ApiError, BranchCommitError, BranchError, BranchRefError, DatabaseError, ObserverError,
};
use serde::Serialize;

use crate::serve::protocol::{WireOwnedCause, WireOwnerKind, WireRefusal};
use crate::serve::receipt_fragments::{EncodedReceipt, ReceiptCodecError};
use crate::serve::receipt_wire::WireCause;

#[derive(Debug, Clone)]
pub enum FailureCause {
    Database(Arc<DatabaseError>),
    #[cfg(test)]
    Api(Arc<ApiError>),
    #[cfg(test)]
    LeafLog(Arc<haematite::LeafLogError>),
    Observer(Arc<ObserverError>),
    Branch(Arc<BranchError>),
    BranchCommit(Arc<BranchCommitError>),
    BranchRef(Arc<BranchRefError>),
}

impl FailureCause {
    fn original(&self) -> &(dyn std::error::Error + 'static) {
        match self {
            Self::Database(error) => error.as_ref(),
            #[cfg(test)]
            Self::Api(error) => error.as_ref(),
            #[cfg(test)]
            Self::LeafLog(error) => error.as_ref(),
            Self::Observer(error) => error.as_ref(),
            Self::Branch(error) => error.as_ref(),
            Self::BranchCommit(error) => error.as_ref(),
            Self::BranchRef(error) => error.as_ref(),
        }
    }

    fn database(&self) -> Option<&DatabaseError> {
        match self {
            Self::Database(error) => Some(error),
            #[cfg(test)]
            Self::Api(error) => match error.as_ref() {
                ApiError::Storage(error) => Some(error),
                _ => None,
            },
            #[cfg(test)]
            Self::LeafLog(error) => match error.as_ref() {
                haematite::LeafLogError::Storage(error) => Some(error),
                _ => None,
            },
            Self::Observer(error) => match error.as_ref() {
                ObserverError::Config(error) | ObserverError::Api(ApiError::Storage(error)) => {
                    Some(error)
                }
                _ => None,
            },
            Self::Branch(_) | Self::BranchCommit(_) | Self::BranchRef(_) => None,
        }
    }

    fn wire(&self) -> Result<WireOwnedCause, ReceiptCodecError> {
        let kind = match self {
            Self::Database(_) => WireOwnerKind::Database,
            #[cfg(test)]
            Self::Api(_) => WireOwnerKind::Api,
            #[cfg(test)]
            Self::LeafLog(_) => WireOwnerKind::LeafLog,
            Self::Observer(_) => WireOwnerKind::Observer,
            Self::Branch(_) => WireOwnerKind::Branch,
            Self::BranchCommit(_) => WireOwnerKind::BranchCommit,
            Self::BranchRef(_) => WireOwnerKind::BranchRef,
        };
        Ok(WireOwnedCause {
            kind,
            message: self.original().to_string(),
            database: self.database().map(WireCause::project).transpose()?,
        })
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct Failure {
    pub code: String,
    pub message: String,
    pub path: Option<String>,
    #[serde(skip)]
    pub(crate) cause: Option<FailureCause>,
}

impl std::fmt::Display for Failure {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}: {}", self.code, self.message)
    }
}
impl std::error::Error for Failure {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.cause.as_ref().map(FailureCause::original)
    }
}
impl From<DatabaseError> for Failure {
    fn from(error: DatabaseError) -> Self {
        Self::database(error)
    }
}

macro_rules! typed_constructor {
    ($name:ident, $variant:ident, $error:ty) => {
        pub(crate) fn $name(error: $error) -> Self {
            Self::owned(FailureCause::$variant(Arc::new(error)))
        }
    };
}

impl Failure {
    pub(crate) fn new(code: &str, message: impl Into<String>) -> Self {
        Self {
            code: code.to_owned(),
            message: message.into(),
            path: None,
            cause: None,
        }
    }
    pub(crate) fn storage(error: impl std::fmt::Display) -> Self {
        Self::new("storage_unavailable", error.to_string())
    }
    typed_constructor!(database, Database, DatabaseError);
    #[cfg(test)]
    typed_constructor!(api, Api, ApiError);
    #[cfg(test)]
    typed_constructor!(leaf_log, LeafLog, haematite::LeafLogError);
    typed_constructor!(observer, Observer, ObserverError);
    typed_constructor!(branch, Branch, BranchError);
    typed_constructor!(branch_commit, BranchCommit, BranchCommitError);
    typed_constructor!(branch_ref, BranchRef, BranchRefError);

    fn owned(cause: FailureCause) -> Self {
        let (code, message) =
            if let Some(DatabaseError::PathWriterRequired { found }) = cause.database() {
                (
                    "store_writer_unsupported",
                    format!("store format stamp {found}; serve writes only bytes-v2 stores"),
                )
            } else {
                ("storage_unavailable", cause.original().to_string())
            };
        Self {
            code: code.to_owned(),
            message,
            path: None,
            cause: Some(cause),
        }
    }

    pub(crate) fn seal(&self) -> Result<Option<EncodedReceipt>, ReceiptCodecError> {
        let Some(cause) = &self.cause else {
            return Ok(None);
        };
        let receipt = match cause.database() {
            Some(DatabaseError::BatchFailed(failure)) => Some(failure.receipt()),
            _ => None,
        };
        EncodedReceipt::refusal(
            receipt,
            WireRefusal {
                code: self.code.clone(),
                message: self.message.clone(),
                path: self.path.clone(),
                cause: Some(cause.wire()?),
            },
        )
        .map(Some)
    }
}