use std::future::Future;
use std::time::Duration;
use super::error::{CapabilityError, ErrorCategory};
use crate::context::Context;
#[derive(Debug, Clone)]
pub enum StoreError {
Unavailable { message: String },
SerializationFailed { message: String },
Conflict { event_id: String },
InvalidQuery { message: String },
AuthFailed { message: String },
RateLimited { retry_after: Duration },
Timeout {
elapsed: Duration,
deadline: Duration,
},
NotFound { message: String },
InvariantViolation { message: String },
Internal { message: String },
}
impl std::fmt::Display for StoreError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unavailable { message } => write!(f, "store unavailable: {message}"),
Self::SerializationFailed { message } => write!(f, "serialization failed: {message}"),
Self::Conflict { event_id } => write!(f, "already exists: {event_id}"),
Self::InvalidQuery { message } => write!(f, "invalid query: {message}"),
Self::AuthFailed { message } => write!(f, "store auth failed: {message}"),
Self::RateLimited { retry_after } => {
write!(f, "rate limited, retry after {retry_after:?}")
}
Self::Timeout { elapsed, deadline } => {
write!(f, "timed out after {elapsed:?} (deadline: {deadline:?})")
}
Self::NotFound { message } => write!(f, "not found: {message}"),
Self::InvariantViolation { message } => write!(f, "invariant violation: {message}"),
Self::Internal { message } => write!(f, "internal store error: {message}"),
}
}
}
impl std::error::Error for StoreError {}
impl CapabilityError for StoreError {
fn category(&self) -> ErrorCategory {
match self {
Self::Unavailable { .. } => ErrorCategory::Unavailable,
Self::SerializationFailed { .. } | Self::InvalidQuery { .. } => {
ErrorCategory::InvalidInput
}
Self::Conflict { .. } => ErrorCategory::Conflict,
Self::AuthFailed { .. } => ErrorCategory::Auth,
Self::RateLimited { .. } => ErrorCategory::RateLimit,
Self::Timeout { .. } => ErrorCategory::Timeout,
Self::NotFound { .. } => ErrorCategory::NotFound,
Self::InvariantViolation { .. } => ErrorCategory::InvariantViolation,
Self::Internal { .. } => ErrorCategory::Internal,
}
}
fn is_transient(&self) -> bool {
matches!(
self,
Self::Unavailable { .. } | Self::RateLimited { .. } | Self::Timeout { .. }
)
}
fn is_retryable(&self) -> bool {
matches!(
self,
Self::Unavailable { .. }
| Self::RateLimited { .. }
| Self::Timeout { .. }
| Self::Conflict { .. }
)
}
fn retry_after(&self) -> Option<Duration> {
match self {
Self::RateLimited { retry_after } => Some(*retry_after),
_ => None,
}
}
}
pub trait ContextStore: Send + Sync {
type LoadFut<'a>: Future<Output = Result<Option<Context>, StoreError>> + Send + 'a
where
Self: 'a;
type SaveFut<'a>: Future<Output = Result<(), StoreError>> + Send + 'a
where
Self: 'a;
fn load_context<'a>(&'a self, scope_id: &'a str) -> Self::LoadFut<'a>;
fn save_context<'a>(&'a self, scope_id: &'a str, context: &'a Context) -> Self::SaveFut<'a>;
}
pub type BoxFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait DynContextStore: Send + Sync {
fn load_context<'a>(
&'a self,
scope_id: &'a str,
) -> BoxFuture<'a, Result<Option<Context>, StoreError>>;
fn save_context<'a>(
&'a self,
scope_id: &'a str,
context: &'a Context,
) -> BoxFuture<'a, Result<(), StoreError>>;
}
impl<T: ContextStore> DynContextStore for T {
fn load_context<'a>(
&'a self,
scope_id: &'a str,
) -> BoxFuture<'a, Result<Option<Context>, StoreError>> {
Box::pin(ContextStore::load_context(self, scope_id))
}
fn save_context<'a>(
&'a self,
scope_id: &'a str,
context: &'a Context,
) -> BoxFuture<'a, Result<(), StoreError>> {
Box::pin(ContextStore::save_context(self, scope_id, context))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_error_display() {
let err = StoreError::Conflict {
event_id: "evt-123".to_string(),
};
assert!(err.to_string().contains("evt-123"));
}
#[test]
fn store_error_category_classification() {
assert_eq!(
StoreError::Unavailable {
message: "test".to_string()
}
.category(),
ErrorCategory::Unavailable
);
assert_eq!(
StoreError::Conflict {
event_id: "test".to_string()
}
.category(),
ErrorCategory::Conflict
);
}
#[test]
fn store_error_transient_classification() {
assert!(
StoreError::Unavailable {
message: "test".to_string()
}
.is_transient()
);
assert!(
!StoreError::Conflict {
event_id: "test".to_string()
}
.is_transient()
);
}
#[test]
fn store_error_retryable_classification() {
assert!(
StoreError::Unavailable {
message: "test".to_string()
}
.is_retryable()
);
assert!(
!StoreError::AuthFailed {
message: "test".to_string()
}
.is_retryable()
);
}
#[test]
fn store_error_retry_after() {
let err = StoreError::RateLimited {
retry_after: Duration::from_secs(60),
};
assert_eq!(err.retry_after(), Some(Duration::from_secs(60)));
}
}