use std::fmt;
use std::sync::Arc;
use thiserror::Error;
pub type SharedError = Arc<dyn std::error::Error + Send + Sync + 'static>;
#[derive(Debug)]
struct DisplayError(String);
impl fmt::Display for DisplayError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for DisplayError {}
pub type Result<T> = std::result::Result<T, PhotonError>;
#[derive(Debug, Clone, Error)]
pub enum PhotonError {
#[error("topic not found: {0}")]
TopicNotFound(String),
#[error("subscription not found: {0}")]
SubscriptionNotFound(String),
#[error("event not found: {0}")]
EventNotFound(String),
#[error("invalid topic name: {0}")]
InvalidTopicName(String),
#[error("payload error: {0}")]
PayloadError(String),
#[error("schema mismatch: {0}")]
SchemaMismatch(String),
#[error("topic already exists: {0}")]
TopicAlreadyExists(String),
#[error("subscription name required for durable subscriptions")]
SubscriptionNameRequired,
#[deprecated(note = "use PhotonError::persistence(...) for sourced failures")]
#[error("persistence error: {0}")]
PersistenceError(String),
#[error("persistence error: {context}")]
Persistence {
context: String,
#[source]
source: SharedError,
},
#[error("identity error: {0}")]
Identity(String),
#[error("internal error: {0}")]
Internal(String),
#[error("internal error: {context}")]
Caused {
context: String,
#[source]
source: SharedError,
},
}
impl PhotonError {
pub fn caused(
context: impl Into<String>,
err: impl fmt::Display + Send + Sync + 'static,
) -> Self {
Self::Caused {
context: context.into(),
source: Arc::new(DisplayError(err.to_string())),
}
}
pub fn caused_error(
context: impl Into<String>,
err: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Caused {
context: context.into(),
source: Arc::new(err),
}
}
pub fn persistence(
context: impl Into<String>,
err: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Persistence {
context: context.into(),
source: Arc::new(err),
}
}
}
impl From<serde_json::Error> for PhotonError {
fn from(err: serde_json::Error) -> Self {
Self::PayloadError(err.to_string())
}
}
impl From<photon_core::IdentityError> for PhotonError {
fn from(err: photon_core::IdentityError) -> Self {
match err {
photon_core::IdentityError::InvalidActor(msg)
| photon_core::IdentityError::Factory(msg) => Self::Identity(msg),
}
}
}
impl From<anyhow::Error> for PhotonError {
fn from(err: anyhow::Error) -> Self {
Self::Caused {
context: err.to_string(),
source: Arc::new(DisplayError(format!("{err:#}"))),
}
}
}
#[cfg(test)]
mod tests {
use std::error::Error as _;
use super::*;
#[derive(Debug, thiserror::Error)]
#[error("disk offline")]
struct DiskError;
struct BadTag;
impl fmt::Display for BadTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("bad auth tag")
}
}
#[test]
fn caused_wraps_display_only_source() {
let err = PhotonError::caused("decrypt failed", BadTag);
assert_eq!(err.to_string(), "internal error: decrypt failed");
let source = err.source().expect("caused keeps a source");
assert_eq!(source.to_string(), "bad auth tag");
}
#[test]
fn caused_error_preserves_source_chain() {
let err = PhotonError::caused_error("flush failed", DiskError);
assert_eq!(err.to_string(), "internal error: flush failed");
let source = err.source().expect("caused_error keeps a source");
assert_eq!(source.to_string(), "disk offline");
}
#[test]
fn persistence_reports_context_and_source() {
let err = PhotonError::persistence("sqlite decode", DiskError);
assert_eq!(err.to_string(), "persistence error: sqlite decode");
let source = err.source().expect("persistence keeps a source");
assert_eq!(source.to_string(), "disk offline");
}
#[test]
fn anyhow_conversion_keeps_full_chain() {
let err = anyhow::Error::new(DiskError).context("flush checkpoint");
let err = PhotonError::from(err);
assert_eq!(err.to_string(), "internal error: flush checkpoint");
let source = err.source().expect("anyhow conversion keeps a source");
let chain = source.to_string();
assert!(chain.contains("flush checkpoint"), "chain: {chain}");
assert!(chain.contains("disk offline"), "chain: {chain}");
}
#[test]
fn serde_json_conversion_maps_to_payload_error() {
let err = serde_json::from_str::<serde_json::Value>("{").unwrap_err();
let err = PhotonError::from(err);
assert!(matches!(err, PhotonError::PayloadError(_)));
}
#[test]
fn caused_errors_stay_clone() {
let err = PhotonError::caused("original", BadTag);
let clone = err.clone();
assert_eq!(err.to_string(), clone.to_string());
assert_eq!(
err.source().map(ToString::to_string),
clone.source().map(ToString::to_string)
);
}
}