use std::fmt;
use crate::i18n::tr;
#[cfg(feature = "shutdown")]
use crate::kit::shutdown::ShutdownPhase;
#[derive(Debug)]
pub enum TraitKitError {
CycleDetected {
cycle: Vec<&'static str>,
},
DependencyMissing {
module: &'static str,
missing: &'static str,
},
AlreadyRegistered {
module: &'static str,
},
BuildFailed {
context: String,
source: Box<dyn std::error::Error + Send + 'static>,
},
MissingCapability {
key: String,
},
MissingConfig {
key: String,
},
#[cfg(feature = "lifecycle")]
LifecycleFailed {
context: String,
source: Box<dyn std::error::Error + Send + 'static>,
},
#[cfg(feature = "shutdown")]
ShutdownTimedOut {
phases: Vec<crate::kit::shutdown::ShutdownPhase>,
},
}
impl fmt::Display for TraitKitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CycleDetected { cycle } => {
write!(
f,
"{}",
tr(
"trait-kit-error-cycle-detected",
&[("cycle", &cycle.join(" → "))],
)
)
}
Self::DependencyMissing { module, missing } => {
write!(
f,
"{}",
tr(
"trait-kit-error-dependency-missing",
&[("module", *module), ("missing", *missing)],
)
)
}
Self::AlreadyRegistered { module } => {
write!(
f,
"{}",
tr("trait-kit-error-already-registered", &[("module", *module)]),
)
}
Self::BuildFailed { context, source } => {
let source_str = source.to_string();
write!(
f,
"{}",
tr(
"trait-kit-error-build-failed",
&[("context", context.as_str()), ("source", &source_str)],
)
)
}
Self::MissingCapability { key } => {
write!(
f,
"{}",
tr(
"trait-kit-error-missing-capability",
&[("key", key.as_str())]
),
)
}
Self::MissingConfig { key } => {
write!(
f,
"{}",
tr("trait-kit-error-missing-config", &[("key", key.as_str())]),
)
}
#[cfg(feature = "lifecycle")]
Self::LifecycleFailed { context, source } => {
let source_str = source.to_string();
write!(
f,
"{}",
tr(
"trait-kit-error-lifecycle-failed",
&[("context", context.as_str()), ("source", &source_str)],
)
)
}
#[cfg(feature = "shutdown")]
Self::ShutdownTimedOut { phases } => {
let phase_names: Vec<&str> = phases.iter().map(ShutdownPhase::as_str).collect();
write!(
f,
"{}",
tr(
"trait-kit-error-shutdown-timed-out",
&[("phases", &phase_names.join(", "))],
)
)
}
}
}
}
impl std::error::Error for TraitKitError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::BuildFailed { source, .. } => Some(source.as_ref()),
#[cfg(feature = "lifecycle")]
Self::LifecycleFailed { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}
pub type TraitKitResult<T> = std::result::Result<T, TraitKitError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cycle_detected_display_contains_modules() {
let err = TraitKitError::CycleDetected {
cycle: vec!["A", "B", "C"],
};
let msg = format!("{err}");
assert!(msg.contains('A'), "should contain module A: got '{msg}'");
assert!(msg.contains('B'), "should contain module B: got '{msg}'");
assert!(msg.contains('C'), "should contain module C: got '{msg}'");
}
#[test]
fn dependency_missing_display_contains_both_modules() {
let err = TraitKitError::DependencyMissing {
module: "mod-a",
missing: "mod-b",
};
let msg = format!("{err}");
assert!(msg.contains("mod-a"), "should contain module: got '{msg}'");
assert!(
msg.contains("mod-b"),
"should contain missing dep: got '{msg}'"
);
}
#[test]
fn already_registered_display_contains_module() {
let err = TraitKitError::AlreadyRegistered {
module: "my-module",
};
let msg = format!("{err}");
assert!(
msg.contains("my-module"),
"should contain module name: got '{msg}'"
);
}
#[test]
fn build_failed_display_contains_context_and_source() {
let err = TraitKitError::BuildFailed {
context: "build".into(),
source: Box::new(std::io::Error::other("oops")),
};
let msg = format!("{err}");
assert!(msg.contains("build"), "should contain context: got '{msg}'");
assert!(
msg.contains("oops"),
"should contain source error: got '{msg}'"
);
}
#[test]
fn missing_capability_display_contains_key() {
let err = TraitKitError::MissingCapability { key: "cap".into() };
let msg = format!("{err}");
assert!(msg.contains("cap"), "should contain key: got '{msg}'");
}
#[test]
fn missing_config_display_contains_key() {
let err = TraitKitError::MissingConfig {
key: "db.url".into(),
};
let msg = format!("{err}");
assert!(
msg.contains("db.url"),
"should contain config key: got '{msg}'"
);
}
#[cfg(feature = "lifecycle")]
#[test]
fn lifecycle_failed_display_contains_context_and_source() {
let err = TraitKitError::LifecycleFailed {
context: "on_ready".into(),
source: Box::new(std::io::Error::other("fail")),
};
let msg = format!("{err}");
assert!(
msg.contains("on_ready"),
"should contain context: got '{msg}'"
);
assert!(msg.contains("fail"), "should contain source: got '{msg}'");
}
#[test]
fn error_source_returns_inner_for_build_failed() {
let err = TraitKitError::BuildFailed {
context: "build".into(),
source: Box::new(std::io::Error::other("oops")),
};
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn error_source_returns_none_for_simple_variants() {
let err = TraitKitError::MissingConfig { key: "x".into() };
assert!(std::error::Error::source(&err).is_none());
}
#[cfg(feature = "lifecycle")]
#[test]
fn error_source_returns_inner_for_lifecycle_failed() {
let err = TraitKitError::LifecycleFailed {
context: "on_ready".into(),
source: Box::new(std::io::Error::other("fail")),
};
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn error_debug_format() {
let err = TraitKitError::MissingConfig { key: "x".into() };
let debug = format!("{err:?}");
assert!(debug.contains("MissingConfig"));
}
}