use std::{error::Error, fmt};
use eventsourcingdb::request_options::{
Bound, BoundType, Ordering, ReadEventMissingStrategy, ReadEventsOptions,
ReadFromLatestEventOptions,
};
use crate::{Categorized, Category, NotARevision, parse_revision};
#[cfg(test)]
mod tests;
const END_OF_REVISIONS: u64 = 1 << 63;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Subjects {
subject: String,
recursive: bool,
}
impl Subjects {
#[must_use]
pub fn tree(subject: impl Into<String>) -> Self {
Self::of(subject.into(), true)
}
#[must_use]
pub fn exact(subject: impl Into<String>) -> Self {
Self::of(subject.into(), false)
}
fn of(subject: String, recursive: bool) -> Self {
require_leading_slash(&subject);
Self { subject, recursive }
}
#[must_use]
pub fn subject(&self) -> &str {
&self.subject
}
#[must_use]
pub fn is_tree(&self) -> bool {
self.recursive
}
}
fn require_leading_slash(subject: &str) {
assert!(
subject.starts_with('/'),
"architecturekit: a subject starts with a slash, which {subject:?} does not"
);
}
#[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(String),
AfterEvent(String),
FromLatestEvent {
subject: String,
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(String),
BeforeEvent(String),
}
#[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) => parse_revision(id)?,
LowerBound::AfterEvent(id) => parse_revision(id)? + 1,
LowerBound::Unbounded | LowerBound::FromLatestEvent { .. } => 0,
};
let end = match &options.upper_bound {
UpperBound::UpToEvent(id) => parse_revision(id)? + 1,
UpperBound::BeforeEvent(id) => parse_revision(id)?,
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
{
require_leading_slash(subject);
assert!(
options.order == Order::OldestFirst,
"architecturekit: the database reads from the latest event of a type only oldest first, \
but got FromLatestEvent({subject:?}, {event_type:?}) and NewestFirst"
);
}
}
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) fn database_options<'a>(
subjects: &'a Subjects,
options: &'a ReadOptions,
) -> ReadEventsOptions<'a> {
let bound = |bound_type, id: &'a String| Bound {
bound_type,
id: id.as_str(),
};
let (lower_bound, from_latest_event) = match &options.lower_bound {
LowerBound::Unbounded => (None, None),
LowerBound::FromEvent(id) => (Some(bound(BoundType::Inclusive, id)), None),
LowerBound::AfterEvent(id) => (Some(bound(BoundType::Exclusive, id)), 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,
ty: event_type,
}),
),
};
let upper_bound = match &options.upper_bound {
UpperBound::Unbounded => None,
UpperBound::UpToEvent(id) => Some(bound(BoundType::Inclusive, id)),
UpperBound::BeforeEvent(id) => Some(bound(BoundType::Exclusive, id)),
};
ReadEventsOptions {
from_latest_event,
lower_bound,
order: match options.order {
Order::OldestFirst => None,
Order::NewestFirst => Some(Ordering::Antichronological),
},
recursive: subjects.recursive,
upper_bound,
}
}
#[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 {
NotARevision(NotARevision),
EmptyRange(EmptyRange),
}
impl From<NotARevision> for ReadOptionsError {
fn from(refusal: NotARevision) -> Self {
Self::NotARevision(refusal)
}
}
impl fmt::Display for ReadOptionsError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotARevision(refusal) => refusal.fmt(formatter),
Self::EmptyRange(refusal) => refusal.fmt(formatter),
}
}
}
impl Error for ReadOptionsError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::NotARevision(refusal) => refusal.source(),
Self::EmptyRange(refusal) => refusal.source(),
}
}
}
impl Categorized for ReadOptionsError {
fn category(&self) -> Option<Category> {
None
}
}