use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind")]
pub enum EffectError {
#[serde(rename = "not_found")]
NotFound {
resource: String,
},
#[serde(rename = "invalid_input")]
InvalidInput {
message: String,
},
#[serde(rename = "network_error")]
NetworkError {
message: String,
},
#[serde(rename = "permission_denied")]
PermissionDenied {
message: String,
},
#[serde(rename = "timeout")]
Timeout {
message: String,
},
#[serde(rename = "custom")]
Custom {
code: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
},
}
impl std::fmt::Display for EffectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EffectError::NotFound { resource } => write!(f, "Not found: {}", resource),
EffectError::InvalidInput { message } => write!(f, "Invalid input: {}", message),
EffectError::NetworkError { message } => write!(f, "Network error: {}", message),
EffectError::PermissionDenied { message } => {
write!(f, "Permission denied: {}", message)
}
EffectError::Timeout { message } => write!(f, "Timeout: {}", message),
EffectError::Custom { code, message, .. } => write!(f, "[{}] {}", code, message),
}
}
}
impl std::error::Error for EffectError {}
impl EffectError {
pub fn not_found(resource: impl Into<String>) -> Self {
EffectError::NotFound {
resource: resource.into(),
}
}
pub fn invalid_input(message: impl Into<String>) -> Self {
EffectError::InvalidInput {
message: message.into(),
}
}
pub fn network_error(message: impl Into<String>) -> Self {
EffectError::NetworkError {
message: message.into(),
}
}
pub fn permission_denied(message: impl Into<String>) -> Self {
EffectError::PermissionDenied {
message: message.into(),
}
}
pub fn timeout(message: impl Into<String>) -> Self {
EffectError::Timeout {
message: message.into(),
}
}
pub fn custom(code: impl Into<String>, message: impl Into<String>) -> Self {
EffectError::Custom {
code: code.into(),
message: message.into(),
data: None,
}
}
pub fn custom_with_data(
code: impl Into<String>,
message: impl Into<String>,
data: Value,
) -> Self {
EffectError::Custom {
code: code.into(),
message: message.into(),
data: Some(data),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_serialization() {
let err = EffectError::NotFound {
resource: "issue/123".to_string(),
};
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("not_found"));
assert!(json.contains("issue/123"));
}
#[test]
fn test_custom_error_with_data() {
let err = EffectError::custom_with_data(
"egregore.signal_failed",
"Signal propagation failed",
serde_json::json!({"retry_count": 3}),
);
let json = serde_json::to_string(&err).unwrap();
assert!(json.contains("custom"));
assert!(json.contains("egregore.signal_failed"));
assert!(json.contains("retry_count"));
}
#[test]
fn test_error_display() {
let err = EffectError::not_found("file/README.md");
assert_eq!(err.to_string(), "Not found: file/README.md");
}
}