use lsp_types::Url;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("LSP protocol error: {0}")]
Protocol(#[from] lsp_server::ProtocolError),
#[error("Failed to send message: {0}")]
MessageSend(String),
#[error("Failed to receive message: {0}")]
MessageReceive(String),
#[error("TOML parse error in {uri}: {message}")]
TomlParse { uri: String, message: String },
#[error("Rust parse error in {uri}: {message}")]
RustParse { uri: String, message: String },
#[error("Invalid environment variable syntax in {uri} at line {line}: {message}")]
EnvVarSyntax {
uri: String,
line: u32,
message: String,
},
#[error("Configuration validation error in {uri}: {message}")]
ConfigValidation { uri: String, message: String },
#[error("Route validation error in {uri}: {message}")]
RouteValidation { uri: String, message: String },
#[error("Dependency injection validation error in {uri}: {message}")]
DiValidation { uri: String, message: String },
#[error("Schema load error: {0}")]
SchemaLoad(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("File I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("HTTP request error: {0}")]
Http(#[from] reqwest::Error),
#[error("Index build error: {0}")]
IndexBuild(String),
#[error("{0}")]
Other(#[from] anyhow::Error),
}
impl Error {
pub fn category(&self) -> ErrorCategory {
match self {
Error::Protocol(_) | Error::MessageSend(_) | Error::MessageReceive(_) => {
ErrorCategory::Protocol
}
Error::TomlParse { .. } | Error::RustParse { .. } | Error::EnvVarSyntax { .. } => {
ErrorCategory::Parse
}
Error::ConfigValidation { .. }
| Error::RouteValidation { .. }
| Error::DiValidation { .. } => ErrorCategory::Validation,
Error::SchemaLoad(_)
| Error::Config(_)
| Error::Io(_)
| Error::Json(_)
| Error::Http(_)
| Error::IndexBuild(_)
| Error::Other(_) => ErrorCategory::System,
}
}
pub fn is_recoverable(&self) -> bool {
match self {
Error::Protocol(_) => true,
Error::MessageSend(_) | Error::MessageReceive(_) => true,
Error::TomlParse { .. } | Error::RustParse { .. } | Error::EnvVarSyntax { .. } => true,
Error::ConfigValidation { .. }
| Error::RouteValidation { .. }
| Error::DiValidation { .. } => true,
Error::SchemaLoad(_) => true, Error::Config(_) => false, Error::Http(_) => true, Error::IndexBuild(_) => true, Error::Io(_) => false, Error::Json(_) => false, Error::Other(_) => false, }
}
pub fn severity(&self) -> ErrorSeverity {
match self {
Error::Protocol(_) => ErrorSeverity::Error,
Error::MessageSend(_) | Error::MessageReceive(_) => ErrorSeverity::Error,
Error::TomlParse { .. } | Error::RustParse { .. } => ErrorSeverity::Warning,
Error::EnvVarSyntax { .. } => ErrorSeverity::Warning,
Error::ConfigValidation { .. }
| Error::RouteValidation { .. }
| Error::DiValidation { .. } => ErrorSeverity::Info,
Error::SchemaLoad(_) => ErrorSeverity::Warning, Error::Config(_) => ErrorSeverity::Error, Error::Http(_) => ErrorSeverity::Warning, Error::IndexBuild(_) => ErrorSeverity::Warning, Error::Io(_) => ErrorSeverity::Error, Error::Json(_) => ErrorSeverity::Error, Error::Other(_) => ErrorSeverity::Error, }
}
pub fn document_uri(&self) -> Option<&str> {
match self {
Error::TomlParse { uri, .. }
| Error::RustParse { uri, .. }
| Error::EnvVarSyntax { uri, .. }
| Error::ConfigValidation { uri, .. }
| Error::RouteValidation { uri, .. }
| Error::DiValidation { uri, .. } => Some(uri),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
Protocol,
Parse,
Validation,
System,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ErrorSeverity {
Info,
Warning,
Error,
}
pub struct ErrorHandler {
verbose: bool,
}
impl ErrorHandler {
pub fn new(verbose: bool) -> Self {
Self { verbose }
}
pub fn handle(&self, error: &Error) -> ErrorHandlingResult {
self.log_error(error);
match error.category() {
ErrorCategory::Protocol => self.handle_protocol_error(error),
ErrorCategory::Parse => self.handle_parse_error(error),
ErrorCategory::Validation => self.handle_validation_error(error),
ErrorCategory::System => self.handle_system_error(error),
}
}
fn log_error(&self, error: &Error) {
match error.severity() {
ErrorSeverity::Error => {
if self.verbose {
tracing::error!("{:?}", error);
} else {
tracing::error!("{}", error);
}
}
ErrorSeverity::Warning => {
if self.verbose {
tracing::warn!("{:?}", error);
} else {
tracing::warn!("{}", error);
}
}
ErrorSeverity::Info => {
if self.verbose {
tracing::info!("{:?}", error);
} else {
tracing::info!("{}", error);
}
}
}
}
fn handle_protocol_error(&self, error: &Error) -> ErrorHandlingResult {
tracing::error!("Protocol error occurred: {}", error);
ErrorHandlingResult {
action: RecoveryAction::RetryConnection,
fallback: None,
notify_client: true,
}
}
fn handle_parse_error(&self, error: &Error) -> ErrorHandlingResult {
tracing::warn!("Parse error occurred: {}", error);
ErrorHandlingResult {
action: RecoveryAction::PartialParse,
fallback: None,
notify_client: true,
}
}
fn handle_validation_error(&self, error: &Error) -> ErrorHandlingResult {
tracing::info!("Validation error occurred: {}", error);
ErrorHandlingResult {
action: RecoveryAction::GenerateDiagnostic,
fallback: None,
notify_client: false, }
}
fn handle_system_error(&self, error: &Error) -> ErrorHandlingResult {
tracing::error!("System error occurred: {}", error);
let (action, fallback) = match error {
Error::SchemaLoad(_) => (RecoveryAction::UseFallback, Some("builtin-schema")),
Error::Http(_) => (RecoveryAction::UseCache, None),
Error::IndexBuild(_) => (RecoveryAction::SkipOperation, None),
_ => (RecoveryAction::Abort, None),
};
ErrorHandlingResult {
action,
fallback: fallback.map(String::from),
notify_client: true,
}
}
}
#[derive(Debug)]
pub struct ErrorHandlingResult {
pub action: RecoveryAction,
pub fallback: Option<String>,
pub notify_client: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryAction {
RetryConnection,
PartialParse,
GenerateDiagnostic,
UseFallback,
UseCache,
SkipOperation,
Abort,
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn toml_parse_error(uri: &Url, message: impl Into<String>) -> Error {
Error::TomlParse {
uri: uri.to_string(),
message: message.into(),
}
}
pub fn rust_parse_error(uri: &Url, message: impl Into<String>) -> Error {
Error::RustParse {
uri: uri.to_string(),
message: message.into(),
}
}
pub fn env_var_syntax_error(uri: &Url, line: u32, message: impl Into<String>) -> Error {
Error::EnvVarSyntax {
uri: uri.to_string(),
line,
message: message.into(),
}
}
pub fn config_validation_error(uri: &Url, message: impl Into<String>) -> Error {
Error::ConfigValidation {
uri: uri.to_string(),
message: message.into(),
}
}
pub fn route_validation_error(uri: &Url, message: impl Into<String>) -> Error {
Error::RouteValidation {
uri: uri.to_string(),
message: message.into(),
}
}
pub fn di_validation_error(uri: &Url, message: impl Into<String>) -> Error {
Error::DiValidation {
uri: uri.to_string(),
message: message.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_category() {
let protocol_err = Error::MessageSend("test error".to_string());
assert_eq!(protocol_err.category(), ErrorCategory::Protocol);
let parse_err = Error::TomlParse {
uri: "file:///test.toml".to_string(),
message: "syntax error".to_string(),
};
assert_eq!(parse_err.category(), ErrorCategory::Parse);
let validation_err = Error::ConfigValidation {
uri: "file:///test.toml".to_string(),
message: "invalid config".to_string(),
};
assert_eq!(validation_err.category(), ErrorCategory::Validation);
let system_err = Error::SchemaLoad("failed to load".to_string());
assert_eq!(system_err.category(), ErrorCategory::System);
}
#[test]
fn test_error_recoverability() {
let protocol_err = Error::MessageSend("test error".to_string());
assert!(protocol_err.is_recoverable());
let parse_err = Error::TomlParse {
uri: "file:///test.toml".to_string(),
message: "syntax error".to_string(),
};
assert!(parse_err.is_recoverable());
let io_err = Error::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
assert!(!io_err.is_recoverable());
}
#[test]
fn test_error_severity() {
let protocol_err = Error::MessageSend("test error".to_string());
assert_eq!(protocol_err.severity(), ErrorSeverity::Error);
let parse_err = Error::TomlParse {
uri: "file:///test.toml".to_string(),
message: "syntax error".to_string(),
};
assert_eq!(parse_err.severity(), ErrorSeverity::Warning);
let validation_err = Error::ConfigValidation {
uri: "file:///test.toml".to_string(),
message: "invalid config".to_string(),
};
assert_eq!(validation_err.severity(), ErrorSeverity::Info);
}
#[test]
fn test_error_document_uri() {
let parse_err = Error::TomlParse {
uri: "file:///test.toml".to_string(),
message: "syntax error".to_string(),
};
assert_eq!(parse_err.document_uri(), Some("file:///test.toml"));
let system_err = Error::SchemaLoad("failed".to_string());
assert_eq!(system_err.document_uri(), None);
}
#[test]
fn test_error_handler() {
let handler = ErrorHandler::new(false);
let protocol_err = Error::MessageSend("test error".to_string());
let result = handler.handle(&protocol_err);
assert_eq!(result.action, RecoveryAction::RetryConnection);
assert!(result.notify_client);
let parse_err = Error::TomlParse {
uri: "file:///test.toml".to_string(),
message: "syntax error".to_string(),
};
let result = handler.handle(&parse_err);
assert_eq!(result.action, RecoveryAction::PartialParse);
assert!(result.notify_client);
let validation_err = Error::ConfigValidation {
uri: "file:///test.toml".to_string(),
message: "invalid config".to_string(),
};
let result = handler.handle(&validation_err);
assert_eq!(result.action, RecoveryAction::GenerateDiagnostic);
assert!(!result.notify_client);
let schema_err = Error::SchemaLoad("failed".to_string());
let result = handler.handle(&schema_err);
assert_eq!(result.action, RecoveryAction::UseFallback);
assert_eq!(result.fallback, Some("builtin-schema".to_string()));
assert!(result.notify_client);
}
#[test]
fn test_error_helper_functions() {
let uri = Url::parse("file:///test.toml").unwrap();
let toml_err = toml_parse_error(&uri, "syntax error");
assert!(matches!(toml_err, Error::TomlParse { .. }));
let rust_err = rust_parse_error(&uri, "syntax error");
assert!(matches!(rust_err, Error::RustParse { .. }));
let env_err = env_var_syntax_error(&uri, 10, "invalid syntax");
assert!(matches!(env_err, Error::EnvVarSyntax { .. }));
let config_err = config_validation_error(&uri, "invalid config");
assert!(matches!(config_err, Error::ConfigValidation { .. }));
let route_err = route_validation_error(&uri, "invalid route");
assert!(matches!(route_err, Error::RouteValidation { .. }));
let di_err = di_validation_error(&uri, "invalid injection");
assert!(matches!(di_err, Error::DiValidation { .. }));
}
}