use std::{any, error::Error, fmt};
use eventsourcingdb::error::{ClientError, code};
use serde_json::Value;
use crate::{Categorized, Category, Failure, Permanent, Revision, Subject};
#[cfg(test)]
mod tests;
pub trait Command {
fn subject(&self) -> Subject;
fn preconditions(&self) -> Vec<Precondition>;
}
pub struct Precondition {
kind: Kind,
}
enum Kind {
Required(eventsourcingdb::Precondition),
OnEventId(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: Subject) -> Self {
Self::require(eventsourcingdb::Precondition::is_subject_pristine(
subject, None,
))
}
#[must_use]
pub fn on_populated_subject(subject: Subject) -> Self {
Self::require(eventsourcingdb::Precondition::is_subject_populated(
subject, None,
))
}
#[must_use]
pub fn on_event_id(subject: Subject, event_id: Revision) -> Self {
Self {
kind: Kind::OnEventId(eventsourcingdb::Precondition::is_subject_on_event_id(
subject,
event_id.to_string(),
None,
)),
}
}
#[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)
}
}
impl PartialEq for Precondition {
fn eq(&self, other: &Self) -> bool {
match (&self.kind, &other.kind) {
(Kind::Required(left), Kind::Required(right))
| (Kind::OnEventId(left), Kind::OnEventId(right)) => {
checked_alike(left) == checked_alike(right)
}
(Kind::OnStateRead, Kind::OnStateRead)
| (Kind::Unconditionally, Kind::Unconditionally) => true,
_ => false,
}
}
}
impl Eq for Precondition {}
impl fmt::Debug for Precondition {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let (made_by, database) = match &self.kind {
Kind::Required(database) => ("require", Some(database)),
Kind::OnEventId(database) => ("on_event_id", Some(database)),
Kind::OnStateRead => ("on_state_read", None),
Kind::Unconditionally => ("unconditionally", None),
};
match database {
Some(database) => write!(
formatter,
"Precondition::{made_by}({})",
checked_alike(database)
),
None => write!(formatter, "Precondition::{made_by}()"),
}
}
}
fn checked_alike(precondition: &eventsourcingdb::Precondition) -> Value {
let mut checked =
serde_json::to_value(precondition).expect("a precondition of the client SDK is JSON");
let unchanged = serde_json::to_value(eventsourcingdb::PreconditionOptions::default())
.expect("the options of a precondition are JSON");
if let Some(payload) = checked.get_mut("payload").and_then(Value::as_object_mut)
&& payload.get("options") == Some(&unchanged)
{
let _ = payload.remove("options");
}
checked
}
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: &Subject,
declared: Vec<Precondition>,
last_event_id: Option<Revision>,
) -> 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.as_str(),
event_id.to_string(),
None,
),
None => eventsourcingdb::Precondition::is_subject_pristine(subject.as_str(), 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");
}
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",
));
}
Ok(())
}
#[derive(Debug)]
pub enum PreconditionsError {
Permanent(Permanent),
}
impl fmt::Display for PreconditionsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Permanent(failure) => failure.fmt(formatter),
}
}
}
impl Error for PreconditionsError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Permanent(failure) => failure.source(),
}
}
}
impl Categorized for PreconditionsError {
fn category(&self) -> Option<Category> {
match self {
Self::Permanent(failure) => failure.category(),
}
}
}