use crate::ServerId;
use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResourceKind {
ToolCount {
server_id: ServerId,
},
ToolNameLength,
DescriptionLength {
tool_name: String,
},
InputSchemaSize {
tool_name: String,
},
OutputSchemaSize {
tool_name: String,
},
GeneratedOutputSize,
GeneratedFileCount,
}
impl fmt::Display for ResourceKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ToolCount { server_id } => write!(f, "tool count for server '{server_id}'"),
Self::ToolNameLength => f.write_str("tool name length"),
Self::DescriptionLength { tool_name } => {
write!(f, "description length for tool '{tool_name}'")
}
Self::InputSchemaSize { tool_name } => {
write!(f, "input_schema size for tool '{tool_name}'")
}
Self::OutputSchemaSize { tool_name } => {
write!(f, "output_schema size for tool '{tool_name}'")
}
Self::GeneratedOutputSize => f.write_str("generated output size"),
Self::GeneratedFileCount => f.write_str("generated file count"),
}
}
}
#[derive(Error, Debug)]
pub enum Error {
#[error("MCP server connection failed: {server}")]
ConnectionFailed {
server: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Security policy violation: {reason}")]
SecurityViolation {
reason: String,
},
#[error("Operation timed out after {duration_secs}s: {operation}")]
Timeout {
operation: String,
duration_secs: u64,
},
#[error("Serialization error: {message}")]
SerializationError {
message: String,
#[source]
source: Option<serde_json::Error>,
},
#[error("Invalid argument: {0}")]
InvalidArgument(String),
#[error("Validation error in {field}: {reason}")]
ValidationError {
field: String,
reason: String,
},
#[error("Script generation failed for tool '{tool}': {message}")]
ScriptGenerationError {
tool: String,
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("resource limit exceeded for {resource}: {actual} exceeds limit of {limit}")]
ResourceLimitExceeded {
resource: ResourceKind,
actual: usize,
limit: usize,
},
#[error("duplicate generated file path: {path}")]
DuplicateGeneratedFilePath {
path: String,
},
}
impl Error {
#[must_use]
pub const fn is_connection_error(&self) -> bool {
matches!(self, Self::ConnectionFailed { .. })
}
#[must_use]
pub const fn is_security_error(&self) -> bool {
matches!(self, Self::SecurityViolation { .. })
}
#[must_use]
pub const fn is_timeout(&self) -> bool {
matches!(self, Self::Timeout { .. })
}
#[must_use]
pub const fn is_validation_error(&self) -> bool {
matches!(self, Self::ValidationError { .. })
}
#[must_use]
pub const fn is_script_generation_error(&self) -> bool {
matches!(self, Self::ScriptGenerationError { .. })
}
#[must_use]
pub const fn is_resource_limit_exceeded(&self) -> bool {
matches!(self, Self::ResourceLimitExceeded { .. })
}
#[must_use]
pub const fn is_duplicate_generated_file_path(&self) -> bool {
matches!(self, Self::DuplicateGeneratedFilePath { .. })
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connection_error_detection() {
let err = Error::ConnectionFailed {
server: "test-server".to_string(),
source: "network error".into(),
};
assert!(err.is_connection_error());
assert!(!err.is_security_error());
}
#[test]
fn test_security_error_detection() {
let err = Error::SecurityViolation {
reason: "Access denied".to_string(),
};
assert!(err.is_security_error());
assert!(!err.is_connection_error());
}
#[test]
fn test_timeout_error_detection() {
let err = Error::Timeout {
operation: "long_operation".to_string(),
duration_secs: 60,
};
assert!(err.is_timeout());
assert!(!err.is_validation_error());
}
#[test]
fn test_error_display() {
let err = Error::SecurityViolation {
reason: "Unauthorized".to_string(),
};
let display = format!("{err}");
assert!(display.contains("Security policy violation"));
assert!(display.contains("Unauthorized"));
}
#[test]
fn test_resource_limit_exceeded_detection() {
let err = Error::ResourceLimitExceeded {
resource: ResourceKind::ToolCount {
server_id: crate::ServerId::new("github").unwrap(),
},
actual: 1500,
limit: 1000,
};
assert!(err.is_resource_limit_exceeded());
assert!(!err.is_security_error());
let display = format!("{err}");
assert!(display.contains("tool count for server 'github'"));
assert!(display.contains("1500"));
assert!(display.contains("1000"));
}
#[test]
fn test_resource_kind_display_variants() {
assert_eq!(ResourceKind::ToolNameLength.to_string(), "tool name length");
assert_eq!(
ResourceKind::DescriptionLength {
tool_name: "send_message".to_string()
}
.to_string(),
"description length for tool 'send_message'"
);
assert_eq!(
ResourceKind::InputSchemaSize {
tool_name: "send_message".to_string()
}
.to_string(),
"input_schema size for tool 'send_message'"
);
assert_eq!(
ResourceKind::OutputSchemaSize {
tool_name: "send_message".to_string()
}
.to_string(),
"output_schema size for tool 'send_message'"
);
assert_eq!(
ResourceKind::GeneratedOutputSize.to_string(),
"generated output size"
);
assert_eq!(
ResourceKind::GeneratedFileCount.to_string(),
"generated file count"
);
}
#[test]
fn test_duplicate_generated_file_path_detection() {
let err = Error::DuplicateGeneratedFilePath {
path: "index.ts".to_string(),
};
assert!(err.is_duplicate_generated_file_path());
assert!(!err.is_resource_limit_exceeded());
let display = format!("{err}");
assert!(display.contains("index.ts"));
}
#[test]
fn test_result_alias() {
#[allow(clippy::unnecessary_wraps)]
fn returns_ok() -> Result<i32> {
Ok(42)
}
fn returns_err() -> Result<i32> {
Err(Error::InvalidArgument("test error".to_string()))
}
assert_eq!(returns_ok().unwrap(), 42);
assert!(returns_err().is_err());
}
}