use std::{any, error::Error, fmt};
use eventsourcingdb::error::{ClientError, code};
use crate::{Categorized, Category, Failure, NotARevision, Permanent, parse_revision};
#[cfg(test)]
mod tests;
pub trait Command {
fn subject(&self) -> String;
fn preconditions(&self) -> Vec<Precondition>;
}
#[derive(Debug)]
pub struct Precondition {
kind: Kind,
}
#[derive(Debug)]
enum Kind {
Required(eventsourcingdb::Precondition),
OnEventId {
event_id: String,
database: eventsourcingdb::Precondition,
},
OnStateRead,
Unconditionally,
}
impl Precondition {
#[must_use]
pub fn require(precondition: eventsourcingdb::Precondition) -> Self {
Self {
kind: Kind::Required(precondition),
}
}
#[must_use]
pub fn on_pristine_subject(subject: impl Into<String>) -> Self {
Self::require(eventsourcingdb::Precondition::is_subject_pristine(
subject, None,
))
}
#[must_use]
pub fn on_populated_subject(subject: impl Into<String>) -> Self {
Self::require(eventsourcingdb::Precondition::is_subject_populated(
subject, None,
))
}
#[must_use]
pub fn on_event_id(subject: impl Into<String>, event_id: impl Into<String>) -> Self {
let event_id = event_id.into();
let database =
eventsourcingdb::Precondition::is_subject_on_event_id(subject, event_id.clone(), None);
Self {
kind: Kind::OnEventId { event_id, database },
}
}
#[must_use]
pub fn on_state_read() -> Self {
Self {
kind: Kind::OnStateRead,
}
}
#[must_use]
pub fn unconditionally() -> Self {
Self {
kind: Kind::Unconditionally,
}
}
#[must_use]
pub fn database(&self) -> Option<&eventsourcingdb::Precondition> {
match &self.kind {
Kind::Required(database) | Kind::OnEventId { database, .. } => Some(database),
Kind::OnStateRead | Kind::Unconditionally => None,
}
}
#[must_use]
pub fn is_on_state_read(&self) -> bool {
matches!(self.kind, Kind::OnStateRead)
}
#[must_use]
pub fn is_unconditional(&self) -> bool {
matches!(self.kind, Kind::Unconditionally)
}
}
pub fn check_preconditions<C: Command + ?Sized>(command: &C) -> Result<(), PreconditionsError> {
checked_preconditions(command).map(|_| ())
}
pub(crate) fn checked_preconditions<C: Command + ?Sized>(
command: &C,
) -> Result<Vec<Precondition>, PreconditionsError> {
let declared = command.preconditions();
check(&declared, any::type_name::<C>())?;
Ok(declared)
}
pub(crate) fn violated_precondition<'a>(
conflict: &Failure,
declared: &'a [Precondition],
) -> Option<&'a Precondition> {
let Some(ClientError::DbApiError(answer)) = conflict.cause()?.downcast_ref::<ClientError>()
else {
return None;
};
if answer.code != code::PRECONDITION_VIOLATED {
return None;
}
let index = answer.details.get("precondition")?.as_u64()?;
declared.get(usize::try_from(index).ok()?)
}
pub(crate) fn resolve_preconditions(
subject: &str,
declared: Vec<Precondition>,
last_event_id: Option<&str>,
) -> Vec<eventsourcingdb::Precondition> {
declared
.into_iter()
.filter_map(|precondition| match precondition.kind {
Kind::Required(database) | Kind::OnEventId { database, .. } => Some(database),
Kind::OnStateRead => Some(match last_event_id {
Some(event_id) => {
eventsourcingdb::Precondition::is_subject_on_event_id(subject, event_id, None)
}
None => eventsourcingdb::Precondition::is_subject_pristine(subject, None),
}),
Kind::Unconditionally => None,
})
.collect()
}
pub(crate) fn write_preconditions(
declared: Vec<Precondition>,
) -> Result<Vec<eventsourcingdb::Precondition>, PreconditionsError> {
let refuse = |message: &str| {
Err(PreconditionsError::Permanent(Permanent::Other(
Failure::new(message, None),
)))
};
if declared.is_empty() {
return refuse(
"a write declares no preconditions, use Precondition::unconditionally to write without any",
);
}
if declared.iter().any(Precondition::is_on_state_read) {
return refuse(
"a write reads no state, so Precondition::on_state_read has nothing to guard, \
use on_pristine_subject, on_populated_subject, on_event_id, or require instead",
);
}
if declared.len() > 1 && declared.iter().any(Precondition::is_unconditional) {
return refuse("a write combines Precondition::unconditionally with other preconditions");
}
for precondition in &declared {
if let Kind::OnEventId { event_id, .. } = &precondition.kind {
let _ = parse_revision(event_id).map_err(PreconditionsError::NotARevision)?;
}
}
Ok(declared
.into_iter()
.filter_map(|precondition| match precondition.kind {
Kind::Required(database) | Kind::OnEventId { database, .. } => Some(database),
Kind::OnStateRead | Kind::Unconditionally => None,
})
.collect())
}
fn check(declared: &[Precondition], command: &str) -> Result<(), PreconditionsError> {
let refuse = |message: String| {
Err(PreconditionsError::Permanent(Permanent::Other(
Failure::new(message, None),
)))
};
if declared.is_empty() {
return refuse(format!(
"{command} declares no preconditions, use Precondition::unconditionally to write without any",
));
}
if declared.len() > 1 && declared.iter().any(Precondition::is_unconditional) {
return refuse(format!(
"{command} combines Precondition::unconditionally with other preconditions",
));
}
for precondition in declared {
if let Kind::OnEventId { event_id, .. } = &precondition.kind {
let _ = parse_revision(event_id).map_err(PreconditionsError::NotARevision)?;
}
}
Ok(())
}
#[derive(Debug)]
pub enum PreconditionsError {
Permanent(Permanent),
NotARevision(NotARevision),
}
impl fmt::Display for PreconditionsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Permanent(failure) => failure.fmt(formatter),
Self::NotARevision(refusal) => refusal.fmt(formatter),
}
}
}
impl Error for PreconditionsError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Permanent(failure) => failure.source(),
Self::NotARevision(refusal) => refusal.source(),
}
}
}
impl Categorized for PreconditionsError {
fn category(&self) -> Option<Category> {
match self {
Self::Permanent(failure) => failure.category(),
Self::NotARevision(refusal) => refusal.category(),
}
}
}