mcpls_core/
error.rs

1//! Error types for mcpls-core.
2//!
3//! This module defines the canonical error type for the library,
4//! following the Microsoft Rust Guidelines for error handling.
5
6use std::path::PathBuf;
7
8/// The main error type for mcpls-core operations.
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11    /// LSP server failed to initialize.
12    #[error("LSP server initialization failed: {message}")]
13    LspInitFailed {
14        /// Description of the initialization failure.
15        message: String,
16    },
17
18    /// LSP server returned an error response.
19    #[error("LSP server error: {code} - {message}")]
20    LspServerError {
21        /// JSON-RPC error code.
22        code: i32,
23        /// Error message from the server.
24        message: String,
25    },
26
27    /// MCP server error.
28    #[error("MCP server error: {0}")]
29    McpServer(String),
30
31    /// Document was not found or could not be opened.
32    #[error("document not found: {0}")]
33    DocumentNotFound(PathBuf),
34
35    /// No LSP server configured for the given language.
36    #[error("no LSP server configured for language: {0}")]
37    NoServerForLanguage(String),
38
39    /// Configuration error.
40    #[error("configuration error: {0}")]
41    Config(String),
42
43    /// Configuration file not found.
44    #[error("configuration file not found: {0}")]
45    ConfigNotFound(PathBuf),
46
47    /// Invalid configuration format.
48    #[error("invalid configuration: {0}")]
49    InvalidConfig(String),
50
51    /// I/O error.
52    #[error("I/O error: {0}")]
53    Io(#[from] std::io::Error),
54
55    /// JSON serialization/deserialization error.
56    #[error("JSON error: {0}")]
57    Json(#[from] serde_json::Error),
58
59    /// TOML parsing error.
60    #[error("TOML parsing error: {0}")]
61    Toml(#[from] toml::de::Error),
62
63    /// LSP client transport error.
64    #[error("transport error: {0}")]
65    Transport(String),
66
67    /// Request timeout.
68    #[error("request timed out after {0} seconds")]
69    Timeout(u64),
70
71    /// Server shutdown requested.
72    #[error("server shutdown requested")]
73    Shutdown,
74
75    /// LSP server failed to spawn.
76    #[error("failed to spawn LSP server '{command}': {source}")]
77    ServerSpawnFailed {
78        /// Command that failed to spawn.
79        command: String,
80        /// Underlying IO error.
81        #[source]
82        source: std::io::Error,
83    },
84
85    /// LSP protocol error during message parsing.
86    #[error("LSP protocol error: {0}")]
87    LspProtocolError(String),
88
89    /// Invalid URI format.
90    #[error("invalid URI: {0}")]
91    InvalidUri(String),
92
93    /// Position encoding error.
94    #[error("position encoding error: {0}")]
95    EncodingError(String),
96
97    /// Server process terminated unexpectedly.
98    #[error("LSP server process terminated unexpectedly")]
99    ServerTerminated,
100
101    /// Invalid tool parameters provided.
102    #[error("invalid tool parameters: {0}")]
103    InvalidToolParams(String),
104
105    /// File I/O error occurred.
106    #[error("file I/O error for {path:?}: {source}")]
107    FileIo {
108        /// Path to the file.
109        path: PathBuf,
110        /// Underlying I/O error.
111        #[source]
112        source: std::io::Error,
113    },
114
115    /// Path is outside allowed workspace boundaries.
116    #[error("path outside workspace: {0}")]
117    PathOutsideWorkspace(PathBuf),
118
119    /// Document limit exceeded.
120    #[error("document limit exceeded: {current}/{max}")]
121    DocumentLimitExceeded {
122        /// Current number of documents.
123        current: usize,
124        /// Maximum allowed documents.
125        max: usize,
126    },
127
128    /// File size limit exceeded.
129    #[error("file size limit exceeded: {size} bytes (max: {max} bytes)")]
130    FileSizeLimitExceeded {
131        /// Actual file size.
132        size: u64,
133        /// Maximum allowed size.
134        max: u64,
135    },
136}
137
138/// A specialized Result type for mcpls-core operations.
139pub type Result<T> = std::result::Result<T, Error>;
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn test_error_display_lsp_init_failed() {
147        let err = Error::LspInitFailed {
148            message: "server not found".to_string(),
149        };
150        assert_eq!(
151            err.to_string(),
152            "LSP server initialization failed: server not found"
153        );
154    }
155
156    #[test]
157    fn test_error_display_lsp_server_error() {
158        let err = Error::LspServerError {
159            code: -32600,
160            message: "Invalid request".to_string(),
161        };
162        assert_eq!(
163            err.to_string(),
164            "LSP server error: -32600 - Invalid request"
165        );
166    }
167
168    #[test]
169    fn test_error_display_document_not_found() {
170        let err = Error::DocumentNotFound(PathBuf::from("/path/to/file.rs"));
171        assert!(err.to_string().contains("document not found"));
172        assert!(err.to_string().contains("file.rs"));
173    }
174
175    #[test]
176    fn test_error_display_no_server_for_language() {
177        let err = Error::NoServerForLanguage("rust".to_string());
178        assert_eq!(
179            err.to_string(),
180            "no LSP server configured for language: rust"
181        );
182    }
183
184    #[test]
185    fn test_error_display_timeout() {
186        let err = Error::Timeout(30);
187        assert_eq!(err.to_string(), "request timed out after 30 seconds");
188    }
189
190    #[test]
191    fn test_error_display_document_limit() {
192        let err = Error::DocumentLimitExceeded {
193            current: 150,
194            max: 100,
195        };
196        assert_eq!(err.to_string(), "document limit exceeded: 150/100");
197    }
198
199    #[test]
200    fn test_error_display_file_size_limit() {
201        let err = Error::FileSizeLimitExceeded {
202            size: 20_000_000,
203            max: 10_000_000,
204        };
205        assert!(err.to_string().contains("file size limit exceeded"));
206    }
207
208    #[test]
209    fn test_error_from_io() {
210        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
211        let err: Error = io_err.into();
212        assert!(matches!(err, Error::Io(_)));
213    }
214
215    #[test]
216    #[allow(clippy::unwrap_used)]
217    fn test_error_from_json() {
218        let json_str = "{invalid json}";
219        let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
220        let err: Error = json_err.into();
221        assert!(matches!(err, Error::Json(_)));
222    }
223
224    #[test]
225    #[allow(clippy::unwrap_used)]
226    fn test_error_from_toml() {
227        let toml_str = "[invalid toml";
228        let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
229        let err: Error = toml_err.into();
230        assert!(matches!(err, Error::Toml(_)));
231    }
232
233    #[test]
234    fn test_result_type_alias() {
235        fn _returns_error() -> Result<i32> {
236            Err(Error::Config("test error".to_string()))
237        }
238
239        let result: Result<i32> = Ok(42);
240        assert!(result.is_ok());
241        if let Ok(value) = result {
242            assert_eq!(value, 42);
243        }
244    }
245
246    #[test]
247    fn test_error_source_chain() {
248        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
249        let err = Error::ServerSpawnFailed {
250            command: "rust-analyzer".to_string(),
251            source: io_err,
252        };
253
254        let source = std::error::Error::source(&err);
255        assert!(source.is_some());
256    }
257}