use eventsourcingdb::error::{ClientError, DbApiError, code};
use status::{CONFLICT, OK, REQUEST_TIMEOUT, TOO_MANY_REQUESTS, UNAUTHORIZED};
use crate::{Failure, Permanent, Transient};
mod status {
pub(super) const OK: u16 = 200;
pub(super) const REQUEST_TIMEOUT: u16 = 408;
pub(super) const CONFLICT: u16 = 409;
pub(super) const TOO_MANY_REQUESTS: u16 = 429;
pub(super) const UNAUTHORIZED: u16 = 401;
}
const TRANSIENT_CODES: [&str; 8] = [
code::SERVER_SHUTTING_DOWN,
code::STORE_READ_ONLY,
code::INDEXES_REBUILDING,
code::RESTORE_IN_PROGRESS,
code::DISK_FULL,
code::STORE_FAILED,
code::LICENSE_INVALID,
code::CAPACITY_EXCEEDED,
];
#[derive(Debug)]
pub(crate) enum Sorted {
Transient(Transient),
Permanent(Permanent),
}
#[derive(Debug)]
pub(crate) enum WriteSorted {
Sorted(Sorted),
OutcomeUnknown(Failure),
}
fn failure(message: String, cause: ClientError) -> Failure {
Failure::new(message, Some(Box::new(cause)))
}
fn transient(message: String, cause: ClientError) -> Sorted {
Sorted::Transient(Transient::Other(failure(message, cause)))
}
fn permanent(message: String, cause: ClientError) -> Sorted {
Sorted::Permanent(Permanent::Other(failure(message, cause)))
}
fn answer_of(error: &ClientError) -> Option<&DbApiError> {
match error {
ClientError::DbApiError(answer) => Some(answer),
_ => None,
}
}
pub(crate) fn database_failure(error: ClientError, doing: &str) -> Sorted {
let answer = match &error {
ClientError::IncompatibleServer => {
return permanent(
format!(
"{doing}: the database does not support what the request needs, and has to be upgraded"
),
error,
);
}
ClientError::InvalidEventType
| ClientError::JsonSchemaError
| ClientError::URLParseError(_) => {
return permanent(doing.to_owned(), error);
}
ClientError::InvalidServerHeader => {
return transient(
format!("{doing}: the answer does not come from an EventSourcingDB"),
error,
);
}
ClientError::DbApiError(answer) => answer,
_ => return transient(doing.to_owned(), error),
};
if !answer.code.is_empty() {
let code = answer.code.as_str();
return if code == code::PRECONDITION_VIOLATED {
Sorted::Transient(Transient::Conflict(failure(doing.to_owned(), error)))
} else if TRANSIENT_CODES.contains(&code) {
transient(doing.to_owned(), error)
} else {
permanent(doing.to_owned(), error)
};
}
match answer.status.as_u16() {
OK | REQUEST_TIMEOUT | TOO_MANY_REQUESTS | 500.. => transient(doing.to_owned(), error),
CONFLICT if answer.description.starts_with("schema conflict") => {
permanent(doing.to_owned(), error)
}
CONFLICT => Sorted::Transient(Transient::Conflict(failure(doing.to_owned(), error))),
UNAUTHORIZED => permanent(
format!("{doing}: the database rejected the API token"),
error,
),
_ => permanent(doing.to_owned(), error),
}
}
pub(crate) fn read_failure(error: ClientError, doing: &str) -> Sorted {
if answer_of(&error)
.is_some_and(|answer| answer.code.is_empty() && answer.status.as_u16() == CONFLICT)
{
return permanent(doing.to_owned(), error);
}
database_failure(error, doing)
}
pub(crate) fn write_failure(error: ClientError, doing: &str) -> WriteSorted {
let ClientError::OutcomeUnknown(inner) = error else {
return WriteSorted::Sorted(database_failure(error, doing));
};
let message = if matches!(*inner, ClientError::InvalidServerHeader) {
format!("{doing}: the answer does not come from an EventSourcingDB")
} else {
doing.to_owned()
};
WriteSorted::OutcomeUnknown(failure(message, *inner))
}
pub(crate) fn is_schema_conflict(refusal: &ClientError) -> bool {
answer_of(refusal).is_some_and(|answer| match answer.code.as_str() {
code::EVENT_SCHEMA_ALREADY_REGISTERED | code::EVENT_SCHEMA_VIOLATED => true,
"" => answer.status.as_u16() == CONFLICT,
_ => false,
})
}
pub(crate) fn schema_refusal(event_type: &str, refusal: ClientError) -> Permanent {
Permanent::Other(failure(
format!("the database refused the schema of {event_type:?}"),
refusal,
))
}