use std::{error::Error, fmt};
use eventsourcingdb::request_options::{
Bound, BoundType, Ordering, ReadEventMissingStrategy, ReadEventsOptions,
ReadFromLatestEventOptions,
};
use crate::{Categorized, Category, Revision, Subject};
#[cfg(test)]
mod tests;
const END_OF_REVISIONS: u64 = 1 << 63;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Subjects {
subject: Subject,
recursive: bool,
}
impl Subjects {
#[must_use]
pub fn tree(subject: Subject) -> Self {
Self {
subject,
recursive: true,
}
}
#[must_use]
pub fn exact(subject: Subject) -> Self {
Self {
subject,
recursive: false,
}
}
#[must_use]
pub fn subject(&self) -> &Subject {
&self.subject
}
#[must_use]
pub fn is_tree(&self) -> bool {
self.recursive
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ReadOptions {
pub lower_bound: LowerBound,
pub upper_bound: UpperBound,
pub order: Order,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum LowerBound {
#[default]
Unbounded,
FromEvent(Revision),
AfterEvent(Revision),
FromLatestEvent {
subject: Subject,
event_type: String,
if_missing: IfLatestEventIsMissing,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IfLatestEventIsMissing {
ReadEverything,
ReadNothing,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum UpperBound {
#[default]
Unbounded,
UpToEvent(Revision),
BeforeEvent(Revision),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Order {
#[default]
OldestFirst,
NewestFirst,
}
pub fn check_read_options(options: &ReadOptions) -> Result<(), ReadOptionsError> {
require_consistent(options);
let first = match &options.lower_bound {
LowerBound::FromEvent(id) => id.get(),
LowerBound::AfterEvent(id) => id.get() + 1,
LowerBound::Unbounded | LowerBound::FromLatestEvent { .. } => 0,
};
let end = match &options.upper_bound {
UpperBound::UpToEvent(id) => id.get() + 1,
UpperBound::BeforeEvent(id) => id.get(),
UpperBound::Unbounded => END_OF_REVISIONS,
};
if first >= end {
return Err(ReadOptionsError::EmptyRange(EmptyRange {
range: describe_range(&options.lower_bound, &options.upper_bound),
}));
}
Ok(())
}
fn require_consistent(options: &ReadOptions) {
if let LowerBound::FromLatestEvent {
subject,
event_type,
..
} = &options.lower_bound
{
assert!(
options.order == Order::OldestFirst,
"architecturekit: the database reads from the latest event of a type only oldest first, \
but got FromLatestEvent({:?}, {event_type:?}) and NewestFirst",
subject.as_str(),
);
}
}
fn describe_range(lower_bound: &LowerBound, upper_bound: &UpperBound) -> String {
let ending = match upper_bound {
UpperBound::Unbounded => {
let (LowerBound::FromEvent(id) | LowerBound::AfterEvent(id)) = lower_bound else {
unreachable!("a range without bounds is never empty");
};
return format!("after \"{id}\"");
}
UpperBound::UpToEvent(id) => format!("up to \"{id}\""),
UpperBound::BeforeEvent(id) => format!("before \"{id}\""),
};
match (lower_bound, upper_bound) {
(LowerBound::Unbounded | LowerBound::FromLatestEvent { .. }, _) => ending,
(LowerBound::AfterEvent(id), _) => format!("after \"{id}\" and {ending}"),
(LowerBound::FromEvent(id), UpperBound::UpToEvent(_)) => {
format!("from \"{id}\" {ending}")
}
(LowerBound::FromEvent(id), _) => format!("from \"{id}\" and {ending}"),
}
}
pub(crate) struct DatabaseBounds {
lower: Option<(BoundType, String)>,
upper: Option<(BoundType, String)>,
}
impl DatabaseBounds {
pub(crate) fn of(options: &ReadOptions) -> Self {
Self {
lower: match &options.lower_bound {
LowerBound::Unbounded | LowerBound::FromLatestEvent { .. } => None,
LowerBound::FromEvent(id) => Some((BoundType::Inclusive, id.to_string())),
LowerBound::AfterEvent(id) => Some((BoundType::Exclusive, id.to_string())),
},
upper: match &options.upper_bound {
UpperBound::Unbounded => None,
UpperBound::UpToEvent(id) => Some((BoundType::Inclusive, id.to_string())),
UpperBound::BeforeEvent(id) => Some((BoundType::Exclusive, id.to_string())),
},
}
}
}
pub(crate) fn database_options<'a>(
subjects: &'a Subjects,
options: &'a ReadOptions,
bounds: &'a DatabaseBounds,
) -> ReadEventsOptions<'a> {
let bound = |bound: &'a Option<(BoundType, String)>| {
bound.as_ref().map(|(bound_type, id)| Bound {
bound_type: bound_type.clone(),
id: id.as_str(),
})
};
let (lower_bound, from_latest_event) = match &options.lower_bound {
LowerBound::Unbounded | LowerBound::FromEvent(_) | LowerBound::AfterEvent(_) => {
(bound(&bounds.lower), None)
}
LowerBound::FromLatestEvent {
subject,
event_type,
if_missing,
} => (
None,
Some(ReadFromLatestEventOptions {
if_event_is_missing: match if_missing {
IfLatestEventIsMissing::ReadEverything => {
ReadEventMissingStrategy::ReadEverything
}
IfLatestEventIsMissing::ReadNothing => ReadEventMissingStrategy::ReadNothing,
},
subject: subject.as_str(),
ty: event_type,
}),
),
};
ReadEventsOptions {
from_latest_event,
lower_bound,
order: match options.order {
Order::OldestFirst => None,
Order::NewestFirst => Some(Ordering::Antichronological),
},
recursive: subjects.recursive,
upper_bound: bound(&bounds.upper),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EmptyRange {
range: String,
}
impl fmt::Display for EmptyRange {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "empty range: no event can lie {}", self.range)
}
}
impl Error for EmptyRange {}
impl Categorized for EmptyRange {
fn category(&self) -> Option<Category> {
None
}
}
#[derive(Debug)]
pub enum ReadOptionsError {
EmptyRange(EmptyRange),
}
impl fmt::Display for ReadOptionsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyRange(refusal) => refusal.fmt(formatter),
}
}
}
impl Error for ReadOptionsError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::EmptyRange(refusal) => refusal.source(),
}
}
}
impl Categorized for ReadOptionsError {
fn category(&self) -> Option<Category> {
None
}
}