use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorCode {
InactiveEffect,
MissingService,
TypeMismatch,
DuplicateService,
AccessDenied,
InvalidConfig,
Plugin,
Event,
PropertyConflict,
Other,
}
impl ErrorCode {
pub const fn message(self) -> &'static str {
match self {
Self::InactiveEffect => "cannot create effect on inactive context",
Self::MissingService => "required service is unavailable",
Self::TypeMismatch => "stored value has an unexpected type",
Self::DuplicateService => "service has already been registered",
Self::AccessDenied => "service belongs to another fiber",
Self::InvalidConfig => "invalid config",
Self::Plugin => "plugin failed",
Self::Event => "event listener failed",
Self::PropertyConflict => "property is already declared",
Self::Other => "cordis error",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationIssue {
pub message: String,
pub path: Vec<String>,
}
impl ValidationIssue {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
path: Vec::new(),
}
}
pub fn at(mut self, path: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.path = path.into_iter().map(Into::into).collect();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
pub issues: Vec<ValidationIssue>,
}
impl ValidationError {
pub fn new(issues: impl IntoIterator<Item = ValidationIssue>) -> Self {
Self {
issues: issues.into_iter().collect(),
}
}
}
impl Display for ValidationError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
writeln!(f, "invalid config:")?;
for (index, issue) in self.issues.iter().enumerate() {
write!(f, " - {}", issue.message)?;
if !issue.path.is_empty() {
write!(f, " (at {})", issue.path.join("."))?;
}
if index + 1 < self.issues.len() {
writeln!(f)?;
}
}
Ok(())
}
}
impl Error for ValidationError {}
#[derive(Debug, Clone)]
pub struct CordisError {
code: ErrorCode,
message: String,
validation: Option<ValidationError>,
source: Option<Arc<dyn Error + Send + Sync + 'static>>,
}
impl CordisError {
pub fn new(code: ErrorCode) -> Self {
Self {
code,
message: code.message().to_owned(),
validation: None,
source: None,
}
}
pub fn with_message(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
validation: None,
source: None,
}
}
pub fn with_source(
code: ErrorCode,
message: impl Into<String>,
source: impl Error + Send + Sync + 'static,
) -> Self {
Self {
code,
message: message.into(),
validation: None,
source: Some(Arc::new(source)),
}
}
pub fn validation(issues: impl IntoIterator<Item = ValidationIssue>) -> Self {
let validation = ValidationError::new(issues);
Self {
code: ErrorCode::InvalidConfig,
message: validation.to_string(),
validation: Some(validation),
source: None,
}
}
pub const fn code(&self) -> ErrorCode {
self.code
}
pub fn validation_error(&self) -> Option<&ValidationError> {
self.validation.as_ref()
}
pub fn context(mut self, context: impl AsRef<str>) -> Self {
self.message = format!("{}: {}", context.as_ref(), self.message);
self
}
pub fn caused_by(mut self, source: impl Error + Send + Sync + 'static) -> Self {
self.source = Some(Arc::new(source));
self
}
}
impl Display for CordisError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl Error for CordisError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_deref()
.map(|source| source as &(dyn Error + 'static))
}
}
impl From<ValidationError> for CordisError {
fn from(value: ValidationError) -> Self {
Self {
code: ErrorCode::InvalidConfig,
message: value.to_string(),
validation: Some(value),
source: None,
}
}
}
impl From<String> for CordisError {
fn from(value: String) -> Self {
Self::with_message(ErrorCode::Other, value)
}
}
impl From<&str> for CordisError {
fn from(value: &str) -> Self {
Self::with_message(ErrorCode::Other, value)
}
}
pub type Result<T, E = CordisError> = std::result::Result<T, E>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_chain_survives_construction_and_clone() {
let cause = std::io::Error::other("disk gone");
let error = CordisError::with_source(ErrorCode::Plugin, "plugin failed", cause);
let source = Error::source(&error).expect("source recorded");
assert_eq!(source.to_string(), "disk gone");
assert_eq!(error.code(), ErrorCode::Plugin);
assert_eq!(error.to_string(), "plugin failed");
let cloned = error.clone();
assert_eq!(
Error::source(&cloned).map(ToString::to_string).as_deref(),
Some("disk gone")
);
let attached = CordisError::new(ErrorCode::Event).caused_by(std::io::Error::other("inner"));
assert_eq!(
Error::source(&attached).map(ToString::to_string).as_deref(),
Some("inner")
);
}
}