use thiserror::Error;
#[derive(Debug, Error)]
pub enum McpifyError {
#[error("{0}")]
Configuration(String),
#[error("{0}")]
Authentication(String),
#[error("{message}{}", format_validation_details(details))]
Validation {
message: String,
details: Option<serde_json::Value>,
},
#[error("{0}")]
NotFound(String),
#[error("circuit breaker is open")]
CircuitBreakerOpen,
#[error("rate limit exceeded")]
RateLimitExceeded,
}
fn format_validation_details(details: &Option<serde_json::Value>) -> String {
match details.as_ref().and_then(serde_json::Value::as_array) {
Some(errors) if !errors.is_empty() => {
let joined = errors
.iter()
.filter_map(serde_json::Value::as_str)
.collect::<Vec<_>>()
.join("; ");
format!(": {joined}")
}
_ => String::new(),
}
}
impl McpifyError {
pub fn code(&self) -> &'static str {
match self {
Self::Configuration(_) => "CONFIGURATION_ERROR",
Self::Authentication(_) => "AUTHENTICATION_ERROR",
Self::Validation { .. } => "VALIDATION_ERROR",
Self::NotFound(_) => "NOT_FOUND",
Self::CircuitBreakerOpen => "CIRCUIT_BREAKER_OPEN",
Self::RateLimitExceeded => "RATE_LIMIT_EXCEEDED",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_variant_reports_its_error_code() {
assert_eq!(
McpifyError::Configuration("x".into()).code(),
"CONFIGURATION_ERROR"
);
assert_eq!(
McpifyError::Authentication("x".into()).code(),
"AUTHENTICATION_ERROR"
);
assert_eq!(
McpifyError::Validation {
message: "x".into(),
details: None
}
.code(),
"VALIDATION_ERROR"
);
assert_eq!(McpifyError::NotFound("x".into()).code(), "NOT_FOUND");
assert_eq!(
McpifyError::CircuitBreakerOpen.code(),
"CIRCUIT_BREAKER_OPEN"
);
assert_eq!(McpifyError::RateLimitExceeded.code(), "RATE_LIMIT_EXCEEDED");
}
#[test]
fn validation_display_appends_details_when_present() {
let err = McpifyError::Validation {
message: "unexpected response shape for 'getAllProjects'".into(),
details: Some(serde_json::json!(["\"foo\" is not of type \"object\""])),
};
assert_eq!(
err.to_string(),
"unexpected response shape for 'getAllProjects': \"foo\" is not of type \"object\""
);
}
#[test]
fn validation_display_omits_suffix_when_details_absent() {
let err = McpifyError::Validation {
message: "invalid input for 'getIssue'".into(),
details: None,
};
assert_eq!(err.to_string(), "invalid input for 'getIssue'");
}
}