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)
}
}