use std::{cmp::Ordering, error::Error, fmt, future::Future, time::Duration};
use crate::{Categorized, Category};
#[cfg(test)]
pub(crate) mod tests;
const LARGEST_REVISION: u64 = (1 << 63) - 1;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NotARevision {
revision: String,
}
impl NotARevision {
#[must_use]
pub fn revision(&self) -> &str {
&self.revision
}
}
impl fmt::Display for NotARevision {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "not a revision: {:?}", self.revision)
}
}
impl Error for NotARevision {}
impl Categorized for NotARevision {
fn category(&self) -> Option<Category> {
None
}
}
pub fn parse_revision(revision: &str) -> Result<u64, NotARevision> {
let refused = || NotARevision {
revision: revision.to_owned(),
};
if revision.is_empty() || !revision.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(refused());
}
match revision.parse::<u64>() {
Ok(number) if number <= LARGEST_REVISION => Ok(number),
_ => Err(refused()),
}
}
pub fn compare_revisions(left: &str, right: &str) -> Result<Ordering, NotARevision> {
let left = revision_number(left)?;
let right = revision_number(right)?;
Ok(left.cmp(&right))
}
pub(crate) fn revision_number(revision: &str) -> Result<Option<u64>, NotARevision> {
if revision.is_empty() {
return Ok(None);
}
parse_revision(revision).map(Some)
}
pub trait Revisioned: Send + Sync {
type Error: Error + Categorized + Send + Sync + 'static;
fn revision(&self) -> impl Future<Output = Result<Option<String>, Self::Error>> + Send;
fn wait_for(&self, revision: &str) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[must_use]
pub fn revision_of(events: &[eventsourcingdb::Event]) -> Option<String> {
events
.iter()
.filter_map(|event| Some((parse_revision(event.id()).ok()?, event.id())))
.max_by_key(|(number, _)| *number)
.map(|(_, id)| id.to_owned())
}
pub async fn wait_for_written<V: Revisioned + ?Sized>(
view: &V,
written: &[eventsourcingdb::Event],
timeout: Duration,
) -> Result<(), NotCaughtUp<V::Error>> {
assert!(
!timeout.is_zero(),
"architecturekit: wait_for_written needs a timeout that is positive, not {timeout:?}",
);
let Some(revision) = revision_of(written) else {
return Ok(());
};
match tokio::time::timeout(timeout, view.wait_for(&revision)).await {
Ok(Ok(())) => Ok(()),
Ok(Err(error)) => Err(NotCaughtUp::View(error)),
Err(_) => Err(NotCaughtUp::TimedOut(timeout)),
}
}
#[derive(Debug)]
pub enum NotCaughtUp<E> {
TimedOut(Duration),
View(E),
}
impl<E> fmt::Display for NotCaughtUp<E> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TimedOut(timeout) => write!(
formatter,
"not caught up: the events were written, but the view did not catch up within {timeout:?}",
),
Self::View(_) => formatter.write_str(
"not caught up: the events were written, but the view could not be asked",
),
}
}
}
impl<E: Error + 'static> Error for NotCaughtUp<E> {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::TimedOut(_) => None,
Self::View(error) => Some(error),
}
}
}
impl<E> Categorized for NotCaughtUp<E> {
fn category(&self) -> Option<Category> {
None
}
}