use crate::{DependencyStatus, FiberState, InstanceId, PluginId, TypeKey};
use std::panic::Location;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ServiceReadFailure {
Undeclared,
Unavailable(DependencyStatus),
OutOfScope,
Inactive,
TypeMismatch,
InterceptDenied,
InterceptPanicked,
InterceptReentrant,
}
impl std::fmt::Display for ServiceReadFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Undeclared => f.write_str("dependency not declared"),
Self::Unavailable(status) => write!(f, "declared dependency unavailable ({status})"),
Self::OutOfScope => f.write_str("instance outside caller's fiber ancestry"),
Self::Inactive => f.write_str("caller context inactive"),
Self::TypeMismatch => f.write_str("service key has a different value type"),
Self::InterceptDenied => f.write_str("strict read denied by interceptor"),
Self::InterceptPanicked => f.write_str("strict read interceptor panicked"),
Self::InterceptReentrant => f.write_str("same-key strict read interceptor reentered"),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("strict service read {key:?} by fiber {plugin_id:?} instance {instance} at {location}: {reason}")]
#[non_exhaustive]
pub struct ServiceReadError {
pub key: TypeKey,
pub plugin_id: PluginId,
pub instance: InstanceId,
pub location: &'static Location<'static>,
pub reason: ServiceReadFailure,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ServiceWriteFailure {
WrongOwner,
Stale,
InterceptDenied,
InterceptPanicked,
InterceptReentrant,
}
impl std::fmt::Display for ServiceWriteFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WrongOwner => f.write_str("writer called outside its provider fiber"),
Self::Stale => {
f.write_str("service binding or provider generation is no longer active")
}
Self::InterceptDenied => f.write_str("service update denied by interceptor"),
Self::InterceptPanicked => f.write_str("service update interceptor panicked"),
Self::InterceptReentrant => {
f.write_str("same-key service update interceptor reentered")
}
}
}
}
#[derive(Debug, thiserror::Error)]
#[error(
"service write {key:?} by fiber {provider:?} generation {generation} at {location}: {reason}"
)]
#[non_exhaustive]
pub struct ServiceWriteError {
pub key: TypeKey,
pub provider: PluginId,
pub generation: u64,
pub location: &'static Location<'static>,
pub reason: ServiceWriteFailure,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CordisError {
#[error(transparent)]
ServiceRead(#[from] ServiceReadError),
#[error("service {0:?} not found in scope")]
ServiceNotFound(String),
#[error("plugin failed: {0}")]
PluginFailed(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("multiple errors: {errors:?}")]
Aggregate {
errors: Vec<Arc<CordisError>>,
},
#[error("fiber disposed")]
InactiveEffect,
#[error("stale plugin context: load generation {expected}, current generation {current}")]
StaleGeneration { expected: u64, current: u64 },
#[error("plugin context generation {generation} is inactive in state {state:?}")]
InactiveGeneration { generation: u64, state: FiberState },
#[error("root has been shut down")]
Closed,
#[error("config validation failed: {issues:?}")]
Validation { issues: Vec<String> },
#[error("dependency unsatisfied: {0:?}")]
InjectUnsatisfied(Vec<String>),
#[error("service {0:?} already registered in scope")]
ServiceExists(String),
#[error("instance {instance} is outside the caller's fiber subtree")]
InstanceOutOfScope { instance: InstanceId },
#[error("synchronous event {key:?} reentered on the same bus")]
ReentrantEvent { key: TypeKey },
#[error(transparent)]
SyncEventPanicked(Arc<CordisError>),
}
pub type ErrorSink = Arc<dyn Fn(Arc<CordisError>) + Send + Sync>;
pub(crate) fn default_sink() -> ErrorSink {
Arc::new(|e: Arc<CordisError>| eprintln!("[rutis] {}", format_for_sink(&e)))
}
fn format_for_sink(error: &CordisError) -> String {
fn write_source(source: &(dyn std::error::Error + 'static), out: &mut String, indent: usize) {
if let Some(error) = source.downcast_ref::<CordisError>() {
write_error(error, out, indent);
return;
}
out.push_str(&source.to_string());
if let Some(next) = source.source() {
out.push('\n');
out.push_str(&" ".repeat(indent + 2));
out.push_str("caused by: ");
write_source(next, out, indent + 2);
}
}
fn write_error(error: &CordisError, out: &mut String, indent: usize) {
match error {
CordisError::Aggregate { errors } => {
out.push_str("multiple errors:");
for (index, member) in errors.iter().enumerate() {
out.push('\n');
out.push_str(&" ".repeat(indent));
out.push_str(&format!("{}. ", index + 1));
write_error(member, out, indent + 3);
}
}
CordisError::PluginFailed(source) => {
let display = error.to_string();
let direct = source.to_string();
match display.strip_suffix(&direct) {
Some(prefix) => {
out.push_str(prefix);
write_source(source.as_ref(), out, indent);
}
None => {
out.push_str(&display);
if let Some(next) = source.source() {
out.push('\n');
out.push_str(&" ".repeat(indent + 2));
out.push_str("caused by: ");
write_source(next, out, indent + 2);
}
}
}
}
_ => out.push_str(&error.to_string()),
}
}
let mut out = String::new();
write_error(error, &mut out, 0);
out
}
pub(crate) fn aggregate_errors(errors: Vec<CordisError>) -> Option<CordisError> {
match errors.len() {
0 => None,
1 => errors.into_iter().next(),
_ => Some(CordisError::Aggregate {
errors: errors.into_iter().map(Arc::new).collect(),
}),
}
}
pub(crate) fn aggregate_arcs(errors: Vec<Arc<CordisError>>) -> Option<Arc<CordisError>> {
match errors.len() {
0 => None,
1 => errors.into_iter().next(),
_ => Some(Arc::new(CordisError::Aggregate { errors })),
}
}
pub(crate) fn panic_error(p: Box<dyn std::any::Any + Send>) -> CordisError {
let msg = if let Some(s) = p.downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = p.downcast_ref::<String>() {
s.clone()
} else {
"panic in task".to_string()
};
CordisError::PluginFailed(msg.into())
}
pub(crate) fn join_panic_error(join_err: tokio::task::JoinError) -> CordisError {
if join_err.is_panic() {
panic_error(join_err.into_panic())
} else {
CordisError::PluginFailed("task cancelled before completion".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, thiserror::Error)]
#[error("outer cause")]
struct OuterCause {
#[source]
source: InnerCause,
}
#[derive(Debug, thiserror::Error)]
#[error("inner cause")]
struct InnerCause;
#[test]
fn plugin_failure_display_and_sink_preserve_each_cause_once() {
let error = CordisError::PluginFailed(Box::new(OuterCause { source: InnerCause }));
assert_eq!(error.to_string(), "plugin failed: outer cause");
assert_eq!(
format_for_sink(&error),
"plugin failed: outer cause\n caused by: inner cause"
);
assert_eq!(
std::error::Error::source(&error).unwrap().to_string(),
"outer cause"
);
}
#[test]
fn sink_formats_aggregate_members_without_debug_noise() {
let error = CordisError::Aggregate {
errors: vec![
Arc::new(CordisError::PluginFailed("cleanup detail".into())),
Arc::new(CordisError::ServiceNotFound("missing".into())),
],
};
assert_eq!(
format_for_sink(&error),
"multiple errors:\n1. plugin failed: cleanup detail\n2. service \"missing\" not found in scope"
);
}
#[test]
fn sink_recurses_into_aggregate_used_as_plugin_failure_source() {
let nested = CordisError::Aggregate {
errors: vec![Arc::new(CordisError::ServiceNotFound("nested".into()))],
};
let error = CordisError::PluginFailed(Box::new(nested));
assert_eq!(
format_for_sink(&error),
"plugin failed: multiple errors:\n1. service \"nested\" not found in scope"
);
}
}