use std::{error::Error, fmt, future::Future, str::FromStr, time::Duration};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use crate::{Categorized, Category, Failure, Permanent};
#[cfg(test)]
pub(crate) mod tests;
const LARGEST_REVISION: u64 = (1 << 63) - 1;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Revision(u64);
impl Revision {
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
}
impl From<u32> for Revision {
fn from(number: u32) -> Self {
Self(u64::from(number))
}
}
impl TryFrom<u64> for Revision {
type Error = NotARevision;
fn try_from(number: u64) -> Result<Self, NotARevision> {
if number > LARGEST_REVISION {
return Err(NotARevision {
revision: number.to_string(),
});
}
Ok(Self(number))
}
}
impl FromStr for Revision {
type Err = NotARevision;
fn from_str(revision: &str) -> Result<Self, 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(Self(number)),
_ => Err(refused()),
}
}
}
impl fmt::Display for Revision {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, formatter)
}
}
impl Serialize for Revision {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for Revision {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(RevisionVisitor)
}
}
struct RevisionVisitor;
impl de::Visitor<'_> for RevisionVisitor {
type Value = Revision;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a revision, which is a string of a decimal from 0 to 2^63-1")
}
fn visit_str<E: de::Error>(self, revision: &str) -> Result<Revision, E> {
revision.parse().map_err(E::custom)
}
}
#[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(crate) fn id_of(event: &eventsourcingdb::Event) -> Result<Revision, Permanent> {
event.id().parse().map_err(|refusal: NotARevision| {
Permanent::Other(Failure::new(
format!(
"event {:?} on {:?} has an ID that is not a revision",
event.id(),
event.subject(),
),
Some(Box::new(refusal)),
))
})
}
pub trait Revisioned: Send + Sync {
type Error: Error + Categorized + Send + Sync + 'static;
fn revision(&self) -> impl Future<Output = Result<Option<Revision>, Self::Error>> + Send;
fn wait_for(&self, revision: Revision) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[must_use]
pub fn revision_of(events: &[eventsourcingdb::Event]) -> Option<Revision> {
events
.iter()
.filter_map(|event| event.id().parse::<Revision>().ok())
.max()
}
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
}
}