use std::{error::Error, fmt, future::Future};
use chrono::{DateTime, Utc};
use crate::{Categorized, Category, Events, Failure, Permanent, Transient, UpcastEvent, Upcasters};
mod run;
#[cfg(test)]
pub(crate) mod tests;
pub use run::{
CaughtUpError, Phase, ProjectionOptions, ProjectionRun, ProjectionStatus, Reconnect,
wait_caught_up,
};
#[derive(Clone, Debug, PartialEq)]
pub struct Envelope<E> {
pub id: String,
pub time: DateTime<Utc>,
pub source: String,
pub subject: String,
pub event_type: String,
pub trace_parent: Option<String>,
pub trace_state: Option<String>,
pub part: usize,
pub data: E,
}
mod sealed {
pub trait Sealed {}
impl<E: crate::Events + Send + 'static> Sealed for E {}
impl Sealed for eventsourcingdb::Event {}
}
pub trait Projected: sealed::Sealed + Sized + Send + 'static {
#[doc(hidden)]
fn project(
stored: &eventsourcingdb::Event,
upcasters: Option<&Upcasters>,
) -> Result<Vec<Envelope<Self>>, Permanent>;
}
impl<E: Events + Send + 'static> Projected for E {
fn project(
stored: &eventsourcingdb::Event,
upcasters: Option<&Upcasters>,
) -> Result<Vec<Envelope<Self>>, Permanent> {
let upcast = UpcastEvent::from_stored(stored);
let parts = match upcasters {
Some(upcasters) => upcasters.apply(upcast)?,
None => vec![upcast],
};
let mut envelopes = Vec::new();
for (part, event) in parts.into_iter().enumerate() {
let (id, time, source, subject, event_type) = (
event.id().to_owned(),
*event.time(),
event.source().to_owned(),
event.subject().to_owned(),
event.event_type().to_owned(),
);
let data = match E::from_data(&event_type, event.into_data()) {
None => continue,
Some(Ok(data)) => data,
Some(Err(cause)) => {
return Err(Permanent::Other(Failure::new(
format!("decoding event {id} of type {event_type:?}"),
Some(Box::new(cause)),
)));
}
};
envelopes.push(Envelope {
id,
time,
source,
subject,
event_type,
trace_parent: stored.traceparent().map(ToOwned::to_owned),
trace_state: stored.tracestate().map(ToOwned::to_owned),
part,
data,
});
}
Ok(envelopes)
}
}
impl Projected for eventsourcingdb::Event {
fn project(
stored: &eventsourcingdb::Event,
_: Option<&Upcasters>,
) -> Result<Vec<Envelope<Self>>, Permanent> {
Ok(vec![Envelope {
id: stored.id().to_owned(),
time: *stored.time(),
source: stored.source().to_owned(),
subject: stored.subject().to_owned(),
event_type: stored.ty().to_owned(),
trace_parent: stored.traceparent().map(ToOwned::to_owned),
trace_state: stored.tracestate().map(ToOwned::to_owned),
part: 0,
data: stored.clone(),
}])
}
}
pub trait Projection: Send + Sync + 'static {
type Event: Projected;
type Error: Error + Categorized + Send + Sync + 'static;
fn apply(
&self,
event: Envelope<Self::Event>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
#[must_use]
fn upcasters() -> Option<&'static Upcasters> {
None
}
fn seen(&self, event_id: &str) {
let _ = event_id;
}
}
pub trait ResumableProjection: Projection {
fn checkpoint(&self) -> impl Future<Output = Result<Option<String>, Self::Error>> + Send;
fn save_checkpoint(
&self,
event_id: &str,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
#[must_use]
fn catch_up_batch_size(&self) -> usize {
1
}
}
pub trait TransactionalProjection: Send + Sync + 'static {
type Event: Projected;
type Error: Error + Categorized + Send + Sync + 'static;
type Transaction: Transaction<Event = Self::Event, Error = Self::Error>;
fn checkpoint(&self) -> impl Future<Output = Result<Option<String>, Self::Error>> + Send;
fn begin(&self) -> impl Future<Output = Result<Self::Transaction, Self::Error>> + Send;
#[must_use]
fn upcasters() -> Option<&'static Upcasters> {
None
}
#[must_use]
fn catch_up_batch_size(&self) -> usize {
1
}
}
pub trait Transaction: Send {
type Event;
type Error;
fn apply(
&mut self,
event: Envelope<Self::Event>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn commit(self, last_event_id: &str) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn rollback(self) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[derive(Debug)]
pub enum ProjectionError {
Transient(Transient),
Permanent(Permanent),
Panicked {
message: String,
},
Projection {
category: Option<Category>,
error: Box<dyn Error + Send + Sync + 'static>,
},
RolledBack {
cause: Box<ProjectionError>,
rollback: Box<ProjectionError>,
},
}
impl ProjectionError {
pub(crate) fn of<E: Error + Categorized + Send + Sync + 'static>(error: E) -> Self {
Self::Projection {
category: error.category(),
error: Box::new(error),
}
}
pub(crate) fn panicked(payload: &(dyn std::any::Any + Send)) -> Self {
let message = payload
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| payload.downcast_ref::<&str>().copied())
.unwrap_or("a value that is no text");
Self::Panicked {
message: message.to_owned(),
}
}
pub(crate) fn has_panicked(&self) -> bool {
match self {
Self::Panicked { .. } => true,
Self::RolledBack { cause, rollback } => cause.has_panicked() || rollback.has_panicked(),
Self::Transient(_) | Self::Permanent(_) | Self::Projection { .. } => false,
}
}
}
impl From<Permanent> for ProjectionError {
fn from(failure: Permanent) -> Self {
Self::Permanent(failure)
}
}
impl fmt::Display for ProjectionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Transient(failure) => failure.fmt(formatter),
Self::Permanent(failure) => failure.fmt(formatter),
Self::Panicked { message } => write!(
formatter,
"{}: the projection panicked: {message}",
Category::Permanent
),
Self::Projection { error, .. } => error.fmt(formatter),
Self::RolledBack { cause, rollback } => {
write!(formatter, "{cause}, and rolling back failed: {rollback}")
}
}
}
}
impl Error for ProjectionError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Transient(failure) => failure.source(),
Self::Permanent(failure) => failure.source(),
Self::Panicked { .. } => None,
Self::Projection { error, .. } => error.source(),
Self::RolledBack { cause, .. } => Some(cause.as_ref()),
}
}
}
impl Categorized for ProjectionError {
fn category(&self) -> Option<Category> {
match self {
Self::Transient(failure) => failure.category(),
Self::Permanent(failure) => failure.category(),
Self::Panicked { .. } => Some(Category::Permanent),
Self::Projection { category, .. } => *category,
Self::RolledBack { rollback, .. } if rollback.has_panicked() => rollback.category(),
Self::RolledBack { cause, .. } => cause.category(),
}
}
}